Skip to content

Commit 3d89dc1

Browse files
committed
✨ add Bookmark Manager Example
1 parent 6b4110e commit 3d89dc1

File tree

2 files changed

+300
-0
lines changed

2 files changed

+300
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ sqlite.lua 💫
77

88
- [Changelog](https://github.com/tami5/sqlite.lua/blob/master/CHANGELOG.md)
99
- [Docs](https://github.com/tami5/sqlite.lua/blob/master/doc/sqlite.txt)
10+
- [Examples](https://github.com/tami5/sqlite.lua/blob/master/examples)
11+
- [Powered By sqlite.lua](https://github.com/tami5/sqlite.lua#-powered-by-sqlitelua)
1012

1113
<p align="center"> <img src="./doc/preview.svg"> </p>
1214

lua/sqlite/examples/bookmarks.lua

+298
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
---@brief [[
2+
---
3+
--- Web/File Bookmark Database.
4+
--- Inspired by @sunjon implementation of https://entries.wikipedia.org/wiki/Frecency.
5+
--- This file is meant to demonstrate what can be done with `sqlite.lua` with
6+
--- a number of recommendations.
7+
---
8+
---@brief ]]
9+
10+
--- Require the module:
11+
-------------------------
12+
13+
local sqlite = require "sqlite.db" --- for constructing sql databases
14+
local tbl = require "sqlite.tbl" --- for constructing sql tables
15+
local uri = "/tmp/bm_db_v1" -- defined here to be deleted later
16+
17+
--- sqlite builtin helper functions
18+
local julianday, strftime = sqlite.lib.julianday, sqlite.lib.strftime
19+
20+
--- Define and iterate over tables rows/schema:
21+
-------------------------------------------------
22+
---This upfront investment would help you have clear idea, help other
23+
---contribute, and provide you awesome auto-completions with the help of
24+
---EmmyLua.
25+
26+
---@alias BMType '"web"' | '"file"' | '"dir"'
27+
28+
29+
--[[ Datashapes ---------------------------------------------
30+
31+
---@class BMCollection
32+
---@field title string: collection title
33+
34+
---@class BMEntry
35+
---@field id number: unique id
36+
---@field link string: file or web link.
37+
---@field title string: the title of the bookmark.
38+
---@field doc number: date of creation.
39+
---@field type BMType
40+
---@field count number: number of times it was clicked.
41+
---@field collection string: foreign key referencing BMCollection.title.
42+
43+
---@class BMTimeStamp
44+
---@field id number: unique id
45+
---@field timestamp number
46+
---@field entry number: foreign key referencing BMEntry
47+
48+
--]]
49+
50+
--[[ sqlite classes ------------------------------------------
51+
52+
---@class BMEntryTable: sqlite_tbl
53+
54+
---@class BMDatabase: sqlite_db
55+
---@field entries BMEntryTable
56+
---@field collection sqlite_tbl
57+
---@field ts sqlite_tbl
58+
59+
--]]
60+
61+
--- 3. Construct
62+
---------------------------
63+
64+
---@type BMEntryTable
65+
local entries = tbl("entries", {
66+
id = true, -- same as { type = "integer", required = true, primary = true }
67+
link = { "text", required = true },
68+
title = "text",
69+
since = { "date", default = strftime("%s", "now") },
70+
count = { "number", default = 0 },
71+
type = { "text", required = true },
72+
collection = {
73+
type = "text",
74+
reference = "collection.title",
75+
on_update = "cascade", -- means when collection get updated update
76+
on_delete = "null", -- means when collection get deleted, set to null
77+
},
78+
})
79+
80+
---@type BMDatabase
81+
-- sqlite.lua db object will be injected to every table at evaluation.
82+
-- Though no connection will be open until the first sqlite operation.
83+
local BM = sqlite {
84+
uri = uri,
85+
entries = entries,
86+
collection = { -- Yes, tables can be inlined without requiring sql.table.
87+
title = {
88+
"text",
89+
required = true,
90+
unique = true,
91+
primary = true,
92+
},
93+
},
94+
ts = {
95+
_name = "timetamps", -- use this as table name not the field key.
96+
id = true,
97+
timestamp = { "real", default = julianday "now" },
98+
entry = {
99+
type = "integer",
100+
reference = "entries.id",
101+
on_delete = "cascade", --- when referenced entry is deleted, delete self
102+
},
103+
},
104+
-- custom sqlite3 options, if you really know what you want. + keep_open + lazy
105+
opts = {},
106+
}
107+
108+
--- Doesn't need to annotated.
109+
local collection, ts = BM.collection, BM.ts
110+
111+
--- 4. Start adding custom methods or override defaults to own it :D
112+
--------------------------------------------------------------------
113+
114+
---Insert timestamp entry
115+
---@param id number
116+
function ts:insert(id)
117+
ts:__insert { entry = id }
118+
end
119+
120+
---Get timestamp for entry.id or all
121+
---@param id number|nil: BMEntry.id
122+
---@return BMTimeStamp
123+
function ts:get(id)
124+
return ts:__get { --- use "self.__get" as backup for overriding default methods.
125+
where = id and {
126+
entry = id,
127+
} or nil,
128+
select = {
129+
age = (strftime("%s", "now") - strftime("%s", "timestamp")) * 24 * 60,
130+
"id",
131+
"timestamp",
132+
"entry",
133+
},
134+
}
135+
end
136+
137+
---Trim timestamps entries
138+
---@param id number: BMEntry.id
139+
function ts:trim(id)
140+
local rows = ts:get(id) -- calling t.get defined above
141+
local trim = rows[(#rows - 10) + 1]
142+
if trim then
143+
ts:remove { id = "<" .. trim.id, entry = id }
144+
return true
145+
end
146+
return false
147+
end
148+
149+
---Update an entry values
150+
---@param row BMEntry
151+
function entries:edit(id, row)
152+
entries:update {
153+
where = { id = id },
154+
set = row,
155+
}
156+
end
157+
158+
---Increment row count by id.
159+
function entries:inc(id)
160+
local row = entries:where { id = id }
161+
entries:update {
162+
where = { id = id },
163+
set = { count = row.count + 1 }
164+
}
165+
ts:insert(id)
166+
ts:trim(id)
167+
end
168+
169+
---Add a row
170+
---@param row BMEntry
171+
function entries:add(row)
172+
173+
if row.collection and not collection:where { title = row.collection } then
174+
collection:insert { title = row.collection }
175+
end
176+
177+
row.type = row.link:match "^%w+://" and "web" or (row.link:match ".-/.+(%..*)$" and "file" or "dir")
178+
179+
local id = entries:insert(row)
180+
if not row.title and row.type == "web" then
181+
local ok, curl = pcall(require, "plenary.curl")
182+
if ok then
183+
curl.get { -- async function
184+
url = row.link,
185+
callback = function(res)
186+
if res.status ~= 200 then
187+
return
188+
end
189+
entries:update {
190+
where = { id = id },
191+
set = { title = res.body:match "title>(.-)<" },
192+
}
193+
end,
194+
}
195+
end
196+
end
197+
198+
ts:insert(id)
199+
200+
return id
201+
end
202+
203+
local ages = {
204+
[1] = { age = 240, value = 100 }, -- past 4 hours
205+
[2] = { age = 1440, value = 80 }, -- past day
206+
[3] = { age = 4320, value = 60 }, -- past 3 days
207+
[4] = { age = 10080, value = 40 }, -- past week
208+
[5] = { age = 43200, value = 20 }, -- past month
209+
[6] = { age = 129600, value = 10 }, -- past 90 days
210+
}
211+
212+
---Get all entries.
213+
---@param q sqlite_query_select: a query to limit the number entries returned.
214+
---@return BMEntry
215+
function entries:get(q)
216+
local items = entries:map(function(entry)
217+
local recency_score = 0
218+
if not entry.count or entry.count == 0 then
219+
entry.score = 0
220+
return entry
221+
end
222+
223+
for _, ts in pairs(ts:get(entry.id)) do
224+
for _, rank in ipairs(ages) do
225+
if ts.age <= rank.age then
226+
recency_score = recency_score + rank.value
227+
goto continue
228+
end
229+
end
230+
::continue::
231+
end
232+
233+
entry.score = entry.count * recency_score / 10
234+
return entry
235+
end, q)
236+
237+
table.sort(items, function(a, b)
238+
return a.score > b.score
239+
end)
240+
241+
return items
242+
end
243+
244+
entries.demo = {
245+
{ title = "My Pull Requests", link = "https://github.com/pulls", collection = "gh" },
246+
{ title = "My Issues", link = "https://github.com/issues", collection = "gh" },
247+
{ title = "System Configuration", link = "~/sys/config", collection = "config" },
248+
{ title = "Neovim Configuration", link = "~/sys/cli/nvim", collection = "config" },
249+
{ title = "Todos Inbox", link = "~/Org/refile.org", collection = "mang" },
250+
{ link = "https://github.com", collection = "quick access" },
251+
}
252+
253+
function entries:seed()
254+
---Seed entries table
255+
for _, row in ipairs(entries.demo) do
256+
entries:add(row)
257+
end
258+
259+
--- Act as if we've used on those entries
260+
for _ = 1, 5, 1 do
261+
entries:inc(4)
262+
end
263+
for _ = 1, 10, 1 do
264+
entries:inc(1)
265+
end
266+
for _ = 1, 3, 1 do
267+
entries:inc(3)
268+
end
269+
end
270+
271+
if entries:count() == 0 then
272+
entries:seed()
273+
end
274+
275+
276+
---Edit an entry --- simple abstraction over entries.update { where = {id = 3}, set = { title = "none" } }
277+
entries:edit(3, { title = "none" })
278+
279+
--- Get first match
280+
assert(entries:where({ title = "none" }).id == 3)
281+
282+
--- Get all entries
283+
vim.inspect(entries:get { where = { type = "web" } })
284+
285+
--- Delete all file or dir type bookmarks
286+
assert(entries:remove { type = {"file", "dir"} })
287+
288+
--- Should be delete
289+
assert(next(entries:get { where = { type = "file" } }) == nil)
290+
291+
vim.wait(500) -- BLOCK TO GET WEB TITLE FOR PLENERY.CURL
292+
293+
-- See all inputs
294+
print(vim.inspect(entries:get()))
295+
296+
vim.defer_fn(function()
297+
vim.loop.fs_unlink(uri)
298+
end, 40000)

0 commit comments

Comments
 (0)