• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Sholtee / ormlite.extensions / 126

pending completion
126

push

appveyor

D,nes Solti
introduce hash based migrations II

319 of 339 relevant lines covered (94.1%)

2.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.13
/SRC/Public/Schema.cs
1
/********************************************************************************
2
* Schema.cs                                                                     *
3
*                                                                               *
4
* Author: Denes Solti                                                           *
5
********************************************************************************/
6
using System;
7
using System.Collections.Generic;
8
using System.ComponentModel;
9
using System.Data;
10
using System.Diagnostics.CodeAnalysis;
11
using System.Linq;
12
using System.Reflection;
13
using System.Security.Cryptography;
14
using System.Text;
15

16
using ServiceStack.DataAnnotations;
17
using ServiceStack.OrmLite;
18

19
namespace Solti.Utils.OrmLite.Extensions
20
{
21
    using Internals;
22
    using Primitives;
23
    using Properties;
24

25
    /// <summary>
26
    /// Represents a database schema.
27
    /// </summary>
28
    [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
29
    public class Schema
30
    {
31
        private const string INITIAL_COMMIT = nameof(INITIAL_COMMIT);
32

33
        private static string GetHash(string str)
34
        {
3✔
35
            using HashAlgorithm algorithm = SHA256.Create();
3✔
36

37
            StringBuilder sb = new();
3✔
38
            foreach (byte b in algorithm.ComputeHash(Encoding.UTF8.GetBytes(str)))
3✔
39
                sb.Append(b.ToString("X2"));
3✔
40
            
41
            return sb.ToString();
3✔
42
        }
3✔
43

44
        #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
45
        private sealed class Migration 
46
        {
47
            [PrimaryKey, AutoId]
48
            public Guid Id { get; set; }
3✔
49

50
            [Required, Index(Unique = true)]
51
            public long CreatedAtUtc { get; set; }
3✔
52

53
            [Required, StringLength(StringLengthAttribute.MaxText)]
54

55
            public string Sql { get; set; }
3✔
56

57
            [Required, Index(Unique = true)]
58
            public string Hash { get; set; }
3✔
59

60
            public bool Skipped { get; set; }
3✔
61

62
            public string? Comment { get; set; }
3✔
63
        }
64
        #pragma warning restore CS8618
65

66
        /// <summary>
67
        /// The <see cref="IDbConnection"/> to access the database.
68
        /// </summary>
69
        public IDbConnection Connection { get; }
3✔
70

71
        /// <summary>
72
        /// Table definitions in this schema.
73
        /// </summary>
74
        public IReadOnlyList<Type> Tables { get; }
3✔
75

76
        /// <summary>
77
        /// Creates a new <see cref="Schema"/> instance.
78
        /// </summary>
79
        public Schema(IDbConnection connection, params Type[] tables)
3✔
80
        {
3✔
81
            Connection = connection ?? throw new ArgumentNullException(nameof(connection));
3✔
82
            Tables = NodeUtils
3✔
83
                .Flatten(tables ?? throw new ArgumentNullException(nameof(tables)))
84
                .Append(typeof(Migration))
85
                .ToArray();
86
        }
3✔
87

88
        /// <summary>
89
        /// Creates a new <see cref="Schema"/> instance.
90
        /// </summary>
91
        public Schema(IDbConnection connection, params Assembly[] asmsToSearch) : this
3✔
92
        (
93
            connection, 
94
            asmsToSearch
95
                .SelectMany(static asm => asm.GetTypes())
3✔
96
                .Where(static type => type.GetCustomAttribute<DataTableAttribute>(inherit: false) is not null)
3✔
97
                .ToArray()
98
        ) {} 
3✔
99

100
        /// <summary>
101
        /// Initializes the schema.
102
        /// </summary>
103
        public void Initialize() 
104
        {
3✔
105
            if (IsInitialized)
3✔
106
                throw new InvalidOperationException(Resources.ALREADY_INITIALIZED);
3✔
107

108
            IOrmLiteDialectProvider dialectProvider = Connection.GetDialectProvider();
3✔
109

110
            using IBulkedDbConnection bulk = Connection.CreateBulkedDbConnection();
3✔
111

112
            foreach (Type table in Tables)
3✔
113
            {
3✔
114
                bulk.ExecuteNonQuery(dialectProvider.ToCreateTableStatement(table));
3✔
115

116
                ValuesAttribute[] initialValues = table.GetCustomAttributes<ValuesAttribute>().ToArray();
3✔
117
                if (initialValues.Length is 0)
3✔
118
                    continue;
3✔
119

120
                var setters = table
3✔
121
                    .GetModelMetadata()
122
                    .FieldDefinitions
123
                    .Select(static def => new
3✔
124
                    {
125
                        Fn = def.PropertyInfo.ToSetter(),
126
                        def.FieldType
127
                    })
128
                    .ToArray();
129

130
                StaticMethod insert = ((Func<object, bool, bool, long>) bulk.Insert)
3✔
131
                    .Method
132
                    .GetGenericMethodDefinition()
133
                    .MakeGenericMethod(table)
134
                    .ToStaticDelegate();
135

136
                StaticMethod factory = (table.GetConstructor(Type.EmptyTypes) ?? throw new MissingMemberException(table.Name, ConstructorInfo.ConstructorName))
3✔
137
                    .ToStaticDelegate();
138

139
                foreach (ValuesAttribute values in initialValues)
3✔
140
                {
3✔
141
                    if (values.Items.Count != setters.Length)
3✔
142
                        throw new InvalidOperationException(Resources.VALUE_COUNT_NOT_MATCH);
×
143

144
                    object inst = factory()!;
3✔
145

146
                    for (int i = 0; i < setters.Length; i++)
3✔
147
                    {
3✔
148
                        object? val = values.Items[i];
3✔
149
                        var setter = setters[i];
3✔
150

151
                        setter.Fn(inst, val is null || val.GetType() == setter.FieldType
3✔
152
                            ? val
153
                            : TypeDescriptor.GetConverter(setter.FieldType).ConvertFrom(val));
154
                    }
3✔
155

156
                    insert(bulk, inst, false, false);
3✔
157
                }
3✔
158
            }
3✔
159

160
            string sql = bulk.ToString();
3✔
161

162
            bulk.Insert(new Migration
3✔
163
            {
164
                Comment = INITIAL_COMMIT,
165
                CreatedAtUtc = DateTime.UtcNow.Ticks,
166
                Sql = sql,
167
                Hash = GetHash(sql)
168
            });
169

170
            //
171
            // Itt felesleges tranzakciot inditani, mivel a DDL operaciok nem tranzakciozhatok (MySQL-ben biztosan):
172
            // https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html
173
            //
174

175
            bulk.Flush();
3✔
176
        }
3✔
177

178
        /// <summary>
179
        /// Returns true if the schema has already been initialized.
180
        /// </summary>
181
        public bool IsInitialized => Connection.TableExists<Migration>() && Connection.Exists<Migration>(m => m.Comment == INITIAL_COMMIT);
3✔
182

183
        /// <summary>
184
        /// Applies the given migration
185
        /// </summary>
186
        public bool ApplyMigration(string sql, string? comment = null)
187
        {
3✔
188
            if (sql is null)
3✔
189
                throw new ArgumentNullException(nameof(sql));
×
190

191
            string hash = GetHash(sql);
3✔
192
            if (Connection.Exists<Migration>(m => m.Hash == hash))
3✔
193
                return false;
3✔
194

195
            using IBulkedDbConnection bulk = Connection.CreateBulkedDbConnection();
3✔
196

197
            bulk.ExecuteNonQuery(sql);
3✔
198
            bulk.Insert(new Migration
3✔
199
            {
200
                CreatedAtUtc = DateTime.UtcNow.Ticks,
201
                Sql = sql,
202
                Hash = hash,
203
                Comment = comment
204
            });
205
            bulk.Flush();
3✔
206

207
            return true;
3✔
208
        }
3✔
209

210
        /// <summary>
211
        /// Applies the given migration in the specific order.
212
        /// </summary>
213
        /// <remarks>
214
        /// <list type="bullet">
215
        /// <item>This method will initialize the schema if necessary. In this case all the passed <paramref name="migrations"/> will be skipped.</item>
216
        /// <item>This method won't take those migrations into account that have been already applied.</item>
217
        /// </list>
218
        /// </remarks>
219
        public bool[] ApplyMigrations(params (string Sql, string? Comment)[] migrations)
220
        {
3✔
221
            bool skipAll = false;
3✔
222

223
            if (!IsInitialized)
3✔
224
            {
3✔
225
                //
226
                // If the schame has not been initialized yet, consider all the given migrations as unnecessary
227
                //
228

229
                Initialize();
3✔
230
                skipAll = true;
3✔
231
            }
3✔
232

233
            using IBulkedDbConnection bulk = Connection.CreateBulkedDbConnection();
3✔
234

235
            bool[] applied = new bool[migrations.Length];
3✔
236

237
            if (skipAll)
3✔
238
            {
3✔
239
                foreach ((string Sql, string? Comment) in migrations)
3✔
240
                {
3✔
241
                    Connection.Insert(new Migration
3✔
242
                    {
243
                        CreatedAtUtc = DateTime.UtcNow.Ticks,
244
                        Sql = Sql,
245
                        Hash = GetHash(Sql),
246
                        Skipped = true,
247
                        Comment = Comment
248
                    });
249
                }
3✔
250
            }
3✔
251
            else
252
            {
3✔
253
                List<string> hashList = Connection.Column<string>
3✔
254
                (
255
                    Connection.From<Migration>().Select(static m => m.Hash)
256
                );
257

258
                for (int i = 0; i < migrations.Length; i++)
3✔
259
                {
3✔
260
                    (string Sql, string? Comment) = migrations[i];
3✔
261

262
                    string hash = GetHash(Sql);
3✔
263
                    if (hashList.Contains(hash))
3✔
264
                        continue;
3✔
265

266
                    bulk.ExecuteNonQuery(Sql);
3✔
267
                    bulk.Insert(new Migration
3✔
268
                    {
269
                        CreatedAtUtc = DateTime.UtcNow.Ticks,
270
                        Sql = Sql,
271
                        Hash = hash,
272
                        Comment = Comment
273
                    });
274

275
                    applied[i] = true;
3✔
276
                }
3✔
277
            }
3✔
278

279
            bulk.Flush();
3✔
280

281
            return applied;
3✔
282
        }
3✔
283

284
        /// <summary>
285
        /// Gets the last migration script.
286
        /// </summary>
287
        public string? GetLastMigration()
288
        {
3✔
289
            string sql = Connection
3✔
290
                .From<Migration>()
291
                .Select()
292
                .OrderByDescending(m => m.CreatedAtUtc)
293
                .Limit(1)
294
                .ToSelectStatement();
295

296
            return Connection
3✔
297
                .Select<Migration>(sql)
298
                .SingleOrDefault()
299
                ?.Sql;
300
        }
3✔
301

302
        /// <summary>
303
        /// Drops the schema.
304
        /// </summary>
305
        public void Drop() =>
306
            //
307
            // Itt semmit nem lehet kotegelten vegrehajtani mivel az IDialectProvider-ben nincs
308
            // sem "ToTableExistsStatement" sem "ToDropTableStatement()" v hasonlo. 
309
            // 
310

311
            Connection.DropTables
3✔
312
            (
313
                Tables.Reverse().ToArray()
314
            );
315
    }
316
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc