1 module utile.db.sqlite;
2 import std, core.sync.mutex, core.sync.rwmutex, etc.c.sqlite3, utile.except, utile.db, utile.misc;
3 
4 final class SQLite
5 {
6 	this(string name)
7 	{
8 		const(char)* p;
9 		auto flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
10 
11 		if (name.empty)
12 		{
13 			flags |= SQLITE_OPEN_MEMORY;
14 		}
15 		else
16 			p = name.toStringz;
17 
18 		sqlite3_open_v2(p, &_db, flags, null) == SQLITE_OK || error;
19 
20 		exec(`pragma foreign_keys = ON;`);
21 		exec(`pragma temp_store = MEMORY;`);
22 		exec(`pragma synchronous = NORMAL;`);
23 	}
24 
25 	~this()
26 	{
27 		_cache.byValue.each!(a => remove(a));
28 		sqlite3_close(_db);
29 	}
30 
31 	void backup(SQLite dest)
32 	{
33 		auto bk = sqlite3_backup_init(dest._db, MainDb, _db, MainDb);
34 		bk || throwError(`cannot init backup`);
35 
36 		scope (exit)
37 		{
38 			sqlite3_backup_finish(bk);
39 		}
40 
41 		sqlite3_backup_step(bk, -1) == SQLITE_DONE || error;
42 	}
43 
44 	void begin() => exec(`begin;`);
45 	void end() => exec(`end;`);
46 	void rollback() => exec(`rollback;`);
47 
48 	mixin DbBase;
49 private:
50 	enum immutable(char)[4] MainDb = `main`;
51 
52 	void exec(const(char)* sql)
53 	{
54 		char* msg;
55 		sqlite3_exec(_db, sql, null, null, &msg);
56 
57 		if (msg)
58 		{
59 			auto s = msg.fromStringz.idup;
60 			sqlite3_free(msg);
61 
62 			throwError!`error executing query: %s`(s);
63 		}
64 	}
65 
66 	void process(sqlite3_stmt* stmt)
67 	{
68 		execute(stmt);
69 		reset(stmt);
70 	}
71 
72 	auto process(A...)(sqlite3_stmt* stmt)
73 	{
74 		auto self = this; // TODO: DMD BUG
75 
76 		struct S
77 		{
78 			this(this) @disable;
79 
80 			~this() => self.reset(stmt);
81 
82 			const empty() => !_hasRow;
83 
84 			void popFront()
85 			in
86 			{
87 				assert(_hasRow);
88 			}
89 			do
90 			{
91 				_hasRow = self.execute(stmt);
92 			}
93 
94 			auto array()
95 			{
96 				ReturnType!front[] res;
97 
98 				for (; _hasRow; popFront)
99 				{
100 					res ~= front;
101 				}
102 
103 				return res;
104 			}
105 
106 			auto front()
107 			in
108 			{
109 				assert(_hasRow);
110 			}
111 			do
112 			{
113 				Tuple!A r;
114 
115 				debug
116 				{
117 					auto N = r.Types.length;
118 					auto cnt = sqlite3_column_count(stmt);
119 
120 					cnt == N || throwError!`expected %u columns, but query returned %u`(N, cnt);
121 				}
122 
123 				foreach (i, ref v; r)
124 				{
125 					alias T = r.Types[i];
126 
127 					static if (isFloatingPoint!T)
128 					{
129 						v = cast(T)sqlite3_column_double(stmt, i);
130 					}
131 					else static if (isIntegral!T)
132 					{
133 						v = cast(T)sqlite3_column_int64(stmt, i);
134 					}
135 					else static if (is(T == string))
136 					{
137 						v = sqlite3_column_text(stmt, i)[0 .. dataLen(i)].idup;
138 					}
139 					else static if (is(T == Blob))
140 					{
141 						v = cast(Blob)sqlite3_column_blob(stmt, i)[0 .. dataLen(i)].dup;
142 					}
143 					else
144 						static assert(false);
145 				}
146 
147 				static if (A.length > 1)
148 				{
149 					return r;
150 				}
151 				else
152 					return r[0];
153 			}
154 
155 		private:
156 			auto dataLen(uint col) => sqlite3_column_bytes(stmt, col);
157 
158 			bool _hasRow;
159 		}
160 
161 		return S(execute(stmt));
162 	}
163 
164 	auto prepare(string sql)
165 	{
166 		if (auto stmt = _cache.get(sql, null))
167 		{
168 			return stmt;
169 		}
170 
171 		sqlite3_stmt* stmt;
172 		sqlite3_prepare_v2(_db, sql.toStringz, cast(uint)sql.length, &stmt, null) == SQLITE_OK || error;
173 
174 		return _cache[sql] = stmt;
175 	}
176 
177 	void bind(A...)(sqlite3_stmt* stmt, A args)
178 	{
179 		debug
180 		{
181 			auto cnt = sqlite3_bind_parameter_count(stmt);
182 			A.length == cnt || throwError!`expected %u parameters to bind, but %u provided`(cnt, A.length);
183 		}
184 
185 		foreach (uint i, v; args)
186 		{
187 			alias T = Unqual!(typeof(v));
188 
189 			uint code;
190 			uint idx = i + 1;
191 
192 			static if (is(T == typeof(null)))
193 			{
194 				code = sqlite3_bind_null(stmt, idx);
195 			}
196 			else static if (isFloatingPoint!T)
197 			{
198 				code = sqlite3_bind_double(stmt, idx, v);
199 			}
200 			else static if (isIntegral!T)
201 			{
202 				code = sqlite3_bind_int64(stmt, idx, v);
203 			}
204 			else static if (is(T == string))
205 			{
206 				const(char)* p;
207 
208 				if (v is DB_NULL_STRING)
209 				{
210 					p = null;
211 				}
212 				else
213 					p = v.length ? v.ptr : DB_NULL_STRING.ptr;
214 
215 				code = sqlite3_bind_text64(stmt, idx, p, v.length, SQLITE_TRANSIENT, SQLITE_UTF8);
216 			}
217 			else static if (is(T == Blob))
218 			{
219 				const(ubyte)* p;
220 
221 				if (v is DB_NULL_BLOB)
222 				{
223 					p = null;
224 				}
225 				else
226 					p = v.length ? v.ptr : DB_NULL_BLOB.ptr;
227 
228 				code = sqlite3_bind_blob64(stmt, idx, p, v.length, SQLITE_TRANSIENT);
229 			}
230 			else
231 				static assert(false);
232 
233 			code == SQLITE_OK || error;
234 		}
235 	}
236 
237 	auto lastId(sqlite3_stmt * ) => sqlite3_last_insert_rowid(_db);
238 	auto affected(sqlite3_stmt * ) => sqlite3_changes(_db);
239 private:
240 	void reset(sqlite3_stmt* stmt)
241 	{
242 		sqlite3_reset(stmt);
243 	}
244 
245 	void remove(sqlite3_stmt* stmt)
246 	{
247 		sqlite3_finalize(stmt);
248 	}
249 
250 	bool execute(sqlite3_stmt* stmt)
251 	{
252 		auto res = sqlite3_step(stmt);
253 		res == SQLITE_ROW || res == SQLITE_DONE || error;
254 		return res == SQLITE_ROW;
255 	}
256 
257 	bool error()
258 	{
259 		return throwError(sqlite3_errmsg(_db).fromStringz.idup);
260 	}
261 
262 	sqlite3* _db;
263 	sqlite3_stmt*[string] _cache;
264 }