diff --git a/CHANGELOG.md b/CHANGELOG.md index df38830..8afe488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased](https://github.com/gilzoide/unity-sqlite-net/compare/1.2.4...HEAD) ### Added - Support for encrypting / decrypting databases by using [SQLite3 Multiple Ciphers](https://utelle.github.io/SQLite3MultipleCiphers/) implementation +- [SQLiteAsset](Runtime/SQLiteAsset.cs): read-only SQLite database Unity assets. + Files with the extensions ".sqlite", ".sqlite2" and ".sqlite3" will be imported as SQLite database assets. + ".csv" files can be imported as SQLite database assets by changing the importer to `SQLite.Editor.Csv.SQLiteAssetCsvImporter` in the Inspector. +- `SQLiteConnection.SerializeToAsset` extension method for serializing a database to an instance of `SQLiteAsset`. +- `SQLiteConnection.ImportCsvToTable` extension method for importing a CSV text stream as a new table inside the database. ### Changed - Update SQLite to 3.50.1 diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..2059c29 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2247b5abd5a1c47bd9c80355cc56c5ef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Csv.meta b/Editor/Csv.meta new file mode 100644 index 0000000..980ee06 --- /dev/null +++ b/Editor/Csv.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a2c02e692b7cf4b58911c97897f8176a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Csv/SQLiteAssetCsvImporter.cs b/Editor/Csv/SQLiteAssetCsvImporter.cs new file mode 100644 index 0000000..5f57281 --- /dev/null +++ b/Editor/Csv/SQLiteAssetCsvImporter.cs @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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. + */ +using System.IO; +using SQLite.Csv; +using UnityEditor.AssetImporters; +using UnityEngine; + +namespace SQLite.Editor.Csv +{ + [ScriptedImporter(0, null, new[] { "csv" })] + public class SQLiteAssetCsvImporter : ScriptedImporter + { + [Header("SQLite asset options")] + [Tooltip("Name of the table that will be created for holding the CSV data inside the database.")] + [SerializeField] private string _tableName = "data"; + + [Tooltip("Flags controlling how the SQLite connection should be opened. 'ReadWrite' and 'Create' flags will be ignored, since SQLite assets are read-only.")] + [SerializeField] private SQLiteOpenFlags _openFlags = SQLiteOpenFlags.ReadOnly; + + [Tooltip("Whether to store DateTime properties as ticks (true) or strings (false).")] + [SerializeField] private bool _storeDateTimeAsTicks = true; + + [Tooltip("Name of the file created for the database inside Streaming Assets folder during builds.\n\n" + + "If empty, the database bytes will be stored in the asset itself.\n\n" + + "Loading databases from Streaming Assets is not supported in Android and WebGL platforms.")] + [SerializeField] private string _streamingAssetsPath; + + + [Header("CSV options")] + [Tooltip("Which separator character will be used when parsing the CSV file.")] + [SerializeField] private CsvReader.SeparatorChar _CSVSeparator = CsvReader.SeparatorChar.Comma; + + [Tooltip("If true, the original CSV file will also be imported as a TextAsset")] + [SerializeField] private bool _importCSVTextAsset = false; + + [Header("Additional SQL")] + [Tooltip("SQL script that will be run before reading CSV data. Use this for configuring the generated database using PRAGMAs like 'page_size'.")] + [SerializeField, Multiline] private string _SQLBeforeReadingCSV = ""; + + [Tooltip("SQL script that will be run after reading CSV data. Use this for changing the table's schema, creating indices, etc.")] + [SerializeField, Multiline] private string _SQLAfterReadingCSV = ""; + + public override void OnImportAsset(AssetImportContext ctx) + { + SQLiteAsset asset; + using (var tempDb = new SQLiteConnection("")) + using (var file = File.OpenRead(assetPath)) + using (var stream = new StreamReader(file)) + { + if (!string.IsNullOrWhiteSpace(_SQLBeforeReadingCSV)) + { + tempDb.Execute(_SQLBeforeReadingCSV); + } + tempDb.ImportCsvToTable(_tableName, stream, _CSVSeparator); + if (!string.IsNullOrWhiteSpace(_SQLAfterReadingCSV)) + { + tempDb.Execute(_SQLAfterReadingCSV); + } + + asset = tempDb.SerializeToAsset(null, _openFlags, _storeDateTimeAsTicks, _streamingAssetsPath); + } + ctx.AddObjectToAsset("sqlite", asset); + ctx.SetMainObject(asset); + + if (_importCSVTextAsset) + { + var textAsset = new TextAsset(File.ReadAllText(assetPath)) + { + name = $"{Path.GetFileNameWithoutExtension(assetPath)}", + }; + ctx.AddObjectToAsset("text", textAsset); + } + } + } +} diff --git a/Editor/Csv/SQLiteAssetCsvImporter.cs.meta b/Editor/Csv/SQLiteAssetCsvImporter.cs.meta new file mode 100644 index 0000000..7e88486 --- /dev/null +++ b/Editor/Csv/SQLiteAssetCsvImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc7f33370e31747c8b998403bea5e202 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Gilzoide.SqliteNet.Editor.asmdef b/Editor/Gilzoide.SqliteNet.Editor.asmdef new file mode 100644 index 0000000..c2056c7 --- /dev/null +++ b/Editor/Gilzoide.SqliteNet.Editor.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Gilzoide.SqliteNet.Editor", + "rootNamespace": "SQLite.Editor", + "references": [ + "GUID:17f96cd3b93974f6493e51a2f25c1241" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/Gilzoide.SqliteNet.Editor.asmdef.meta b/Editor/Gilzoide.SqliteNet.Editor.asmdef.meta new file mode 100644 index 0000000..6728a4b --- /dev/null +++ b/Editor/Gilzoide.SqliteNet.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: bf1b03bab81fb4dbf9f325bfe2f10d29 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Icons.meta b/Editor/Icons.meta new file mode 100644 index 0000000..3e9bf56 --- /dev/null +++ b/Editor/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 196a203c3f399425bbfae08500a87e46 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Icons/d_solar_database-bold.png b/Editor/Icons/d_solar_database-bold.png new file mode 100644 index 0000000..7cce7a9 Binary files /dev/null and b/Editor/Icons/d_solar_database-bold.png differ diff --git a/Editor/Icons/d_solar_database-bold.png.meta b/Editor/Icons/d_solar_database-bold.png.meta new file mode 100644 index 0000000..eeab7e0 --- /dev/null +++ b/Editor/Icons/d_solar_database-bold.png.meta @@ -0,0 +1,192 @@ +fileFormatVersion: 2 +guid: 73dc51d7e8228432bad9736cede37eb6 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: VisionOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: tvOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Icons/d_solar_database-bold@2x.png b/Editor/Icons/d_solar_database-bold@2x.png new file mode 100644 index 0000000..2449e8c Binary files /dev/null and b/Editor/Icons/d_solar_database-bold@2x.png differ diff --git a/Editor/Icons/d_solar_database-bold@2x.png.meta b/Editor/Icons/d_solar_database-bold@2x.png.meta new file mode 100644 index 0000000..67c73f8 --- /dev/null +++ b/Editor/Icons/d_solar_database-bold@2x.png.meta @@ -0,0 +1,192 @@ +fileFormatVersion: 2 +guid: 319f3bdfbe91a4af8aac8c8710acc4dc +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: VisionOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: tvOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Icons/solar_database-bold.png b/Editor/Icons/solar_database-bold.png new file mode 100644 index 0000000..26b015b Binary files /dev/null and b/Editor/Icons/solar_database-bold.png differ diff --git a/Editor/Icons/solar_database-bold.png.meta b/Editor/Icons/solar_database-bold.png.meta new file mode 100644 index 0000000..6b80e92 --- /dev/null +++ b/Editor/Icons/solar_database-bold.png.meta @@ -0,0 +1,192 @@ +fileFormatVersion: 2 +guid: d05789d285ed2446a97f5c3f99ef13b6 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: VisionOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: tvOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Icons/solar_database-bold@2x.png b/Editor/Icons/solar_database-bold@2x.png new file mode 100644 index 0000000..ae25b70 Binary files /dev/null and b/Editor/Icons/solar_database-bold@2x.png differ diff --git a/Editor/Icons/solar_database-bold@2x.png.meta b/Editor/Icons/solar_database-bold@2x.png.meta new file mode 100644 index 0000000..afac1c2 --- /dev/null +++ b/Editor/Icons/solar_database-bold@2x.png.meta @@ -0,0 +1,192 @@ +fileFormatVersion: 2 +guid: 4f3ac3350a5be4f00917e4fb7aa5a213 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: VisionOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: tvOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SQLiteAssetBuildProcessor.cs b/Editor/SQLiteAssetBuildProcessor.cs new file mode 100644 index 0000000..58fb40f --- /dev/null +++ b/Editor/SQLiteAssetBuildProcessor.cs @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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 !UNITY_ANDROID && !UNITY_WEBGL +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; + +namespace SQLite.Editor +{ + public class SQLiteAssetBuildProcessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport + { + public int callbackOrder => 0; + + public void OnPreprocessBuild(BuildReport report) + { + foreach (SQLiteAsset sqliteAsset in GetAffectedAssets()) + { + string filePath = $"Assets/StreamingAssets/{sqliteAsset.StreamingAssetsPath}"; + string directoryPath = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + File.WriteAllBytes(filePath, sqliteAsset.Bytes); + sqliteAsset.Bytes = Array.Empty(); + } + } + + public void OnPostprocessBuild(BuildReport report) + { + foreach (SQLiteAsset sqliteAsset in GetAffectedAssets()) + { + string filePath = $"Assets/StreamingAssets/{sqliteAsset.StreamingAssetsPath}"; + if (File.Exists(filePath)) + { + sqliteAsset.Bytes = File.ReadAllBytes(filePath); + FileUtil.DeleteFileOrDirectory(filePath); + FileUtil.DeleteFileOrDirectory(filePath + ".meta"); + DeleteEmptyDirectories(Path.GetDirectoryName(filePath)); + } + } + } + + private static void DeleteEmptyDirectories(string directory) + { + while (!string.IsNullOrWhiteSpace(directory)) + { + if (Directory.EnumerateFileSystemEntries(directory).Any()) + { + return; + } + FileUtil.DeleteFileOrDirectory(directory); + FileUtil.DeleteFileOrDirectory(directory + ".meta"); + directory = Path.GetDirectoryName(directory); + } + } + + private static IEnumerable GetAffectedAssets() + { + return AssetDatabase.FindAssets($"t:{nameof(SQLiteAsset)}") + .Select(AssetDatabase.GUIDToAssetPath) + .Select(AssetDatabase.LoadAssetAtPath) + .Where(sqlite => sqlite.UseStreamingAssets); + } + } +} +#endif diff --git a/Editor/SQLiteAssetBuildProcessor.cs.meta b/Editor/SQLiteAssetBuildProcessor.cs.meta new file mode 100644 index 0000000..ec3c44f --- /dev/null +++ b/Editor/SQLiteAssetBuildProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c72b41ff6d7a4557bc93e8c99a1ce09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SQLiteAssetEditor.cs b/Editor/SQLiteAssetEditor.cs new file mode 100644 index 0000000..8a0b666 --- /dev/null +++ b/Editor/SQLiteAssetEditor.cs @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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. + */ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace SQLite.Editor +{ + [CustomEditor(typeof(SQLiteAsset))] + [CanEditMultipleObjects] + public class SQLiteAssetEditor : UnityEditor.Editor + { + [SerializeField] private List _expandedTables = new List(); + + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + if (serializedObject.isEditingMultipleObjects) + { + return; + } + + EditorGUILayout.Space(); + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.TextField("Database size in bytes", EditorUtility.FormatBytes(((SQLiteAsset) target).Bytes.Length)); + } + + EditorGUILayout.Space(); + using (new EditorGUI.DisabledScope(true)) + using (var db = ((SQLiteAsset) target).CreateConnection()) + { + EditorGUILayout.LabelField("Tables", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + foreach ((string name, string sql) in db.Query<(string, string)>("SELECT name, sql FROM SQLite_schema WHERE type = 'table'")) + { + bool previouslyExpanded = _expandedTables.Contains(name); + bool expanded = EditorGUILayout.Foldout(previouslyExpanded, name, true); + if (previouslyExpanded && !expanded) + { + _expandedTables.Remove(name); + } + else if (!previouslyExpanded && expanded) + { + _expandedTables.Add(name); + } + + if (expanded) + { + EditorGUILayout.TextField("SQL", sql); + int count = db.ExecuteScalar($"SELECT COUNT(*) FROM {SQLiteConnection.Quote(name)}"); + EditorGUILayout.IntField("Row Count", count); + } + EditorGUILayout.Space(); + } + EditorGUI.indentLevel--; + } + } + } +} diff --git a/Editor/SQLiteAssetEditor.cs.meta b/Editor/SQLiteAssetEditor.cs.meta new file mode 100644 index 0000000..1d64d2e --- /dev/null +++ b/Editor/SQLiteAssetEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b01081d2adb9418391fe28f13086bcd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SQLiteAssetImporter.cs b/Editor/SQLiteAssetImporter.cs new file mode 100644 index 0000000..9c6ce32 --- /dev/null +++ b/Editor/SQLiteAssetImporter.cs @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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. + */ +using System.IO; +using UnityEditor.AssetImporters; +using UnityEngine; + +namespace SQLite.Editor +{ + [ScriptedImporter(0, new[] { "sqlite", "sqlite2", "sqlite3" })] + public class SQLiteAssetImporter : ScriptedImporter + { + [Tooltip("Flags controlling how the SQLite connection should be opened. 'ReadWrite' and 'Create' flags will be ignored, since SQLite assets are read-only.")] + [SerializeField] private SQLiteOpenFlags _openFlags = SQLiteOpenFlags.ReadOnly; + + [Tooltip("Whether to store DateTime properties as ticks (true) or strings (false).")] + [SerializeField] private bool _storeDateTimeAsTicks = true; + + [Tooltip("Name of the file created for the database inside Streaming Assets folder during builds.\n\n" + + "If empty, the database bytes will be stored in the asset itself.\n\n" + + "Loading databases from Streaming Assets is not supported in Android and WebGL platforms.")] + [SerializeField] private string _streamingAssetsPath; + + public override void OnImportAsset(AssetImportContext ctx) + { + var asset = ScriptableObject.CreateInstance(); + asset.OpenFlags = _openFlags; + asset.StoreDateTimeAsTicks = _storeDateTimeAsTicks; + asset.Bytes = File.ReadAllBytes(ctx.assetPath); + asset.StreamingAssetsPath = _streamingAssetsPath; + ctx.AddObjectToAsset("sqlite", asset); + ctx.SetMainObject(asset); + } + } +} diff --git a/Editor/SQLiteAssetImporter.cs.meta b/Editor/SQLiteAssetImporter.cs.meta new file mode 100644 index 0000000..ec5f3af --- /dev/null +++ b/Editor/SQLiteAssetImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a82a308980f2947468fbbb0695613ed2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index b438b11..c589291 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,20 @@ This package provides the excelent [SQLite-net](https://github.com/praeclarum/sq + Both synchronous and asynchronous APIs are available + `SQLiteConnection.Serialize` extension method for serializing a database to `byte[]` (reference: [SQLite Serialization](https://www.sqlite.org/c3ref/serialize.html)). + `SQLiteConnection.Deserialize` extension method for deserializing memory (`byte[]`, `NativeArray` or `ReadOnlySpan`) into an open database (reference: [SQLite Deserialization](https://www.sqlite.org/c3ref/deserialize.html)). + + `SQLiteConnection.ImportCsvToTable` extension method for importing a CSV text stream as a new table inside the database. - [SQLite3 Multiple Ciphers 2.1.3](https://github.com/utelle/SQLite3MultipleCiphers/releases/tag/v2.1.3) (based on [SQLite 3.50.1](https://sqlite.org/releaselog/3_50_1.html)) + Supports encrypted databases + Enabled modules: [R\*Tree](https://sqlite.org/rtree.html), [Geopoly](https://sqlite.org/geopoly.html), [FTS5](https://sqlite.org/fts5.html), [Built-In Math Functions](https://www.sqlite.org/lang_mathfunc.html) + Supports Windows, Linux, macOS, WebGL, Android, iOS, tvOS and visionOS platforms + Supports persisting data in WebGL builds by using a [custom VFS backed by Indexed DB](https://github.com/gilzoide/idbvfs). - - -## Optional packages -- [SQLite Asset](https://github.com/gilzoide/unity-sqlite-asset): read-only SQLite database assets for Unity with scripted importer for ".sqlite", ".sqlite2" and ".sqlite3" files -- [SQLite Asset - CSV](https://github.com/gilzoide/unity-sqlite-asset-csv): easily import ".csv" files as read-only SQLite database assets - +- [SQLiteAsset](Runtime/SQLiteAsset.cs): read-only SQLite database Unity assets. + + Files with the extensions ".sqlite", ".sqlite2" and ".sqlite3" will be imported as SQLite database assets. + + ".csv" files can be imported as SQLite database assets by changing the importer to `SQLite.Editor.Csv.SQLiteAssetCsvImporter` in the Inspector. + + Use the `CreateConnection()` method for connecting to the database provided by the asset. + Make sure to `Dispose()` of any connections you create. + + SQLite assets may be loaded from Streaming Assets folder or from memory, depending on the value of their "Streaming Assets Path" property. + + **Note**: Android and WebGL platforms don't support loading SQLite databases from Streaming Assets and will always load them in memory. + + `SQLiteConnection.SerializeToAsset` extension method for serializing a database to an instance of `SQLiteAsset`. ## How to install Either: @@ -104,6 +107,8 @@ Third-party code: - SQLite-net: [MIT license](Runtime/sqlite-net/LICENSE.txt) - SQLite3 Multiple Ciphers: [MIT license](https://github.com/utelle/SQLite3MultipleCiphers/blob/main/LICENSE) +Database icons from [Solar Icons Set](https://www.figma.com/community/file/1166831539721848736/solar-icons-set), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) + ## Modifications made to SQLite-net source code - The value of `LibraryPath` was changed from `sqlite3` to `__Internal` in WebGL/iOS/tvOS/visionOS builds and `gilzoide-sqlite-net` for other platforms. diff --git a/Runtime/Csv.meta b/Runtime/Csv.meta new file mode 100644 index 0000000..009e3c9 --- /dev/null +++ b/Runtime/Csv.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b1c2a8809fb9c4735a75582f79b026d4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Csv/CsvException.cs b/Runtime/Csv/CsvException.cs new file mode 100644 index 0000000..334d096 --- /dev/null +++ b/Runtime/Csv/CsvException.cs @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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. + */ +using System; + +namespace SQLite.Csv +{ + public class CsvException : Exception + { + public CsvException(string message) : base(message) {} + } +} diff --git a/Runtime/Csv/CsvException.cs.meta b/Runtime/Csv/CsvException.cs.meta new file mode 100644 index 0000000..2fb9c04 --- /dev/null +++ b/Runtime/Csv/CsvException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 267285bfe909149a9842f0bc1341cf70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Csv/CsvReader.cs b/Runtime/Csv/CsvReader.cs new file mode 100644 index 0000000..872239f --- /dev/null +++ b/Runtime/Csv/CsvReader.cs @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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. + */ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SQLite.Csv +{ + public static class CsvReader + { + public enum SeparatorChar + { + Comma, + Semicolon, + Tabs, + } + + /// + /// Parse a stream of CSV-formatted data. + /// + /// Stream of CSV-formatted data. + /// Character used for separating fields. + /// Maximum field size allowed. + /// + /// The enumeration returns each field's contents, even if it is empty. + /// is returned at the end of the lines, meaning a new row will start next. + /// Empty lines are ignored and will not be enumerated. + /// + /// Thrown if is null. + /// Thrown if any field size is greater than . + public static IEnumerable ParseStream(TextReader stream, SeparatorChar separator = SeparatorChar.Comma, int maxFieldSize = int.MaxValue) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + SkipEmptyLines(stream); + if (stream.Peek() < 0) + { + yield break; + } + + bool insideQuotes = false; + var stringBuilder = new StringBuilder(); + while (true) + { + int c = stream.Read(); + switch (c) + { + case '\r': + if (!insideQuotes && stream.Peek() == '\n') + { + stream.Read(); + goto case '\n'; + } + else + { + goto default; + } + + case '\n': + if (!insideQuotes) + { + yield return stringBuilder.ToString(); + stringBuilder.Clear(); + yield return null; + + SkipEmptyLines(stream); + if (stream.Peek() < 0) + { + yield break; + } + } + else + { + goto default; + } + break; + + case ',': + if (!insideQuotes && separator == SeparatorChar.Comma) + { + yield return stringBuilder.ToString(); + stringBuilder.Clear(); + } + else + { + goto default; + } + break; + + case ';': + if (!insideQuotes && separator == SeparatorChar.Semicolon) + { + yield return stringBuilder.ToString(); + stringBuilder.Clear(); + } + else + { + goto default; + } + break; + + case '\t': + if (!insideQuotes && separator == SeparatorChar.Tabs) + { + yield return stringBuilder.ToString(); + stringBuilder.Clear(); + } + else + { + goto default; + } + break; + + case '"': + if (insideQuotes && stream.Peek() == '"') + { + stream.Read(); + goto default; + } + else + { + insideQuotes = !insideQuotes; + } + break; + + case < 0: + yield return stringBuilder.ToString(); + yield return null; + yield break; + + default: + if (stringBuilder.Length >= maxFieldSize) + { + throw new CsvException("Field size is greater than maximum allowed size."); + } + stringBuilder.Append((char) c); + break; + } + } + } + + private static void SkipEmptyLines(TextReader streamReader) + { + while (true) + { + int c = streamReader.Peek(); + switch (c) + { + case '\n': + case '\r': + streamReader.Read(); + continue; + + default: + return; + } + } + } + } +} diff --git a/Runtime/Csv/CsvReader.cs.meta b/Runtime/Csv/CsvReader.cs.meta new file mode 100644 index 0000000..3df2246 --- /dev/null +++ b/Runtime/Csv/CsvReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6af882ccfbbe24a8fbc2c99371ea98cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SQLiteAsset.cs b/Runtime/SQLiteAsset.cs new file mode 100644 index 0000000..dfb7431 --- /dev/null +++ b/Runtime/SQLiteAsset.cs @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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. + */ +using System; +using UnityEngine; + +namespace SQLite +{ + public class SQLiteAsset : ScriptableObject + { + [Tooltip("Flags controlling how the SQLite connection should be opened. 'ReadWrite' and 'Create' flags will be ignored, since SQLite assets are read-only.")] + [SerializeField] private SQLiteOpenFlags _openFlags = SQLiteOpenFlags.ReadOnly; + + [Tooltip("Whether to store DateTime properties as ticks (true) or strings (false).")] + [SerializeField] private bool _storeDateTimeAsTicks = true; + + [Tooltip("Name of the file created for the database inside Streaming Assets folder during builds.\n\n" + + "If empty, the database bytes will be stored in the asset itself.\n\n" + + "Loading databases from Streaming Assets is not supported in Android and WebGL platforms.")] + [SerializeField] private string _streamingAssetsPath; + + [SerializeField, HideInInspector] private byte[] _bytes; + + /// + /// Flags controlling how the SQLite connection should be opened. + /// + /// + /// 'ReadWrite' and 'Create' flags will be ignored, since SQLite assets are read-only. + /// + public SQLiteOpenFlags OpenFlags + { + get => _openFlags; + set => _openFlags = value & ~(SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); + } + + /// + /// Whether to store DateTime properties as ticks (true) or strings (false). + /// + /// + public bool StoreDateTimeAsTicks + { + get => _storeDateTimeAsTicks; + set => _storeDateTimeAsTicks = value; + } + + /// + /// Bytes that compose the SQLite database file. + /// + public byte[] Bytes + { + get => _bytes; + set => _bytes = value; + } + + /// + /// If true, the database bytes will be read from a file located at the Streaming Assets folder instead of storing all bytes in memory. + /// + public string StreamingAssetsPath + { + get => _streamingAssetsPath; + set => _streamingAssetsPath = value; + } + + /// + /// If true, the database bytes will be read from a file located at the Streaming Assets folder instead of storing all bytes in memory. + /// + public bool UseStreamingAssets => !string.IsNullOrWhiteSpace(_streamingAssetsPath); + + /// + /// Creates a new connection to the read-only SQLite database represented by this asset. + /// + /// + /// If is null. + public SQLiteConnection CreateConnection() + { +#if !UNITY_EDITOR && !UNITY_ANDROID && !UNITY_WEBGL + if (UseStreamingAssets) + { + string path = System.IO.Path.Combine(Application.streamingAssetsPath, _streamingAssetsPath); + return new SQLiteConnection(path); + } +#endif + if (Bytes == null) + { + throw new NullReferenceException(nameof(Bytes)); + } + + return new SQLiteConnection("").Deserialize(Bytes, null, SQLite3.DeserializeFlags.ReadOnly); + } + +#if UNITY_EDITOR + protected void OnValidate() + { + if (_openFlags.HasFlag(SQLiteOpenFlags.ReadWrite)) + { + Debug.LogWarning($"{nameof(SQLiteAsset)} does not support writing to the database. Ignoring \"ReadWrite\" flag.", this); + _openFlags &= ~SQLiteOpenFlags.ReadWrite; + } + if (_openFlags.HasFlag(SQLiteOpenFlags.Create)) + { + Debug.LogWarning($"{nameof(SQLiteAsset)} does not support creating database. Ignoring \"Create\" flag.", this); + _openFlags &= ~SQLiteOpenFlags.Create; + } + } +#endif + } +} diff --git a/Runtime/SQLiteAsset.cs.meta b/Runtime/SQLiteAsset.cs.meta new file mode 100644 index 0000000..c3762b2 --- /dev/null +++ b/Runtime/SQLiteAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f2f61d07f5f634a01b7d297f944a71ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: d05789d285ed2446a97f5c3f99ef13b6, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SQLiteAsyncExtensions.cs b/Runtime/SQLiteAsyncExtensions.cs index 647e1c2..f03727e 100644 --- a/Runtime/SQLiteAsyncExtensions.cs +++ b/Runtime/SQLiteAsyncExtensions.cs @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2025 Gil Barbosa Reis + * + * 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. + */ using System.Threading.Tasks; using UnityEngine; diff --git a/Runtime/SQLiteConnectionExtensions.cs b/Runtime/SQLiteConnectionExtensions.cs index e0b68b6..0d1b812 100644 --- a/Runtime/SQLiteConnectionExtensions.cs +++ b/Runtime/SQLiteConnectionExtensions.cs @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Gil Barbosa Reis + * Copyright (c) 2025 Gil Barbosa Reis * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,11 +20,13 @@ * SOFTWARE. */ using System; +using System.Collections.Generic; +using System.IO; using System.Runtime.InteropServices; -#if UNITY_2018_1_OR_NEWER +using SQLite.Csv; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; -#endif +using UnityEngine; namespace SQLite { @@ -40,7 +42,7 @@ public static byte[] Serialize(this SQLiteConnection db, string schema = null) try { var bytes = new byte[size]; - Marshal.Copy(buffer, bytes, 0, (int) size); + Marshal.Copy(buffer, bytes, 0, (int)size); return bytes; } finally @@ -49,6 +51,16 @@ public static byte[] Serialize(this SQLiteConnection db, string schema = null) } } + public static SQLiteAsset SerializeToAsset(this SQLiteConnection db, string schema = null, SQLiteOpenFlags openFlags = SQLiteOpenFlags.ReadOnly, bool storeDateTimeAsTicks = true, string streamingAssetsPath = null) + { + SQLiteAsset asset = ScriptableObject.CreateInstance(); + asset.Bytes = db.Serialize(schema); + asset.OpenFlags = openFlags; + asset.StoreDateTimeAsTicks = storeDateTimeAsTicks; + asset.StreamingAssetsPath = streamingAssetsPath; + return asset; + } + public static SQLiteConnection Deserialize(this SQLiteConnection db, byte[] buffer, string schema = null, SQLite3.DeserializeFlags flags = SQLite3.DeserializeFlags.None) { return Deserialize(db, buffer, buffer.LongLength, schema, flags); @@ -64,7 +76,6 @@ public static SQLiteConnection Deserialize(this SQLiteConnection db, byte[] buff return db; } -#if UNITY_2018_1_OR_NEWER public static SQLiteConnection Deserialize(this SQLiteConnection db, NativeArray buffer, string schema = null, SQLite3.DeserializeFlags flags = SQLite3.DeserializeFlags.None) { return Deserialize(db, buffer, buffer.Length, schema, flags); @@ -83,9 +94,7 @@ public static SQLiteConnection Deserialize(this SQLiteConnection db, NativeArray } return db; } -#endif -#if UNITY_2021_2_OR_NEWER public static SQLiteConnection Deserialize(this SQLiteConnection db, ReadOnlySpan buffer, string schema = null, SQLite3.DeserializeFlags flags = SQLite3.DeserializeFlags.None) { return Deserialize(db, buffer, buffer.Length, schema, flags); @@ -107,6 +116,64 @@ public static SQLiteConnection Deserialize(this SQLiteConnection db, ReadOnlySpa } return db; } -#endif + + /// + /// Import a CSV data stream into the table named inside the database. + /// The table will be created if it doesn't exist yet. + /// + /// Open database connection + /// Name of the table that should be filled with data from the CSV data stream. + /// Data stream with CSV-formatted contents. + /// Separator used for parsing the CSV. Defaults to comma. + /// Maximum field size allowed. + /// Thrown if any of , and are null. + /// Thrown if an error is found while parsing the CSV data. + public static void ImportCsvToTable(this SQLiteConnection db, string tableName, TextReader csvStream, CsvReader.SeparatorChar separator = CsvReader.SeparatorChar.Comma, int maxFieldSize = int.MaxValue) + { + if (db == null) + { + throw new ArgumentNullException(nameof(db)); + } + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentNullException(nameof(tableName)); + } + if (csvStream == null) + { + throw new ArgumentNullException(nameof(csvStream)); + } + + var columns = new List(); + bool parsingHeader = true; + db.RunInTransaction(() => + { + foreach (string field in CsvReader.ParseStream(csvStream, separator, maxFieldSize)) + { + if (field == null) // newline + { + string joinedColumns = string.Join(", ", columns); + if (parsingHeader) + { + db.Execute($"CREATE TABLE IF NOT EXISTS {tableName} ({joinedColumns})"); + parsingHeader = false; + } + else + { + db.Execute($"INSERT INTO {tableName} VALUES ({joinedColumns})"); + } + columns.Clear(); + } + else + { + if (parsingHeader && string.IsNullOrWhiteSpace(field)) + { + throw new CsvException("Header cannot have empty column name."); + } + + columns.Add(SQLiteConnection.Quote(field)); + } + } + }); + } } } diff --git a/Runtime/SQLiteExtensions.cs b/Runtime/SQLiteExtensions.cs index e682b16..56bc6fc 100644 --- a/Runtime/SQLiteExtensions.cs +++ b/Runtime/SQLiteExtensions.cs @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Gil Barbosa Reis + * Copyright (c) 2025 Gil Barbosa Reis * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/Runtime/SQLitePreparedStatement.cs b/Runtime/SQLitePreparedStatement.cs index 3ff47a6..15be06a 100644 --- a/Runtime/SQLitePreparedStatement.cs +++ b/Runtime/SQLitePreparedStatement.cs @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Gil Barbosa Reis + * Copyright (c) 2025 Gil Barbosa Reis * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json index 969ffd1..9494b99 100644 --- a/package.json +++ b/package.json @@ -14,5 +14,6 @@ "description": "Demonstrates a simple REPL for SQL statements.", "path": "Samples~/REPL" } - ] -} + ], + "unity": "2021.2" +} \ No newline at end of file