Skip to content

Commit

Permalink
feat: add logic to store data by native code (#130)
Browse files Browse the repository at this point in the history
* refactor: make logic for Database be abstract
* feat: add logic for DB by string.dump
* fix: run with async.void to run synchronously
* test: add tests for native feature
* feat!: sort candidates by path when score is same
  This is needed because candidates from SQLite is sorted by id, but ones from native is sorted by path.
* chore: clean up types
* feat: add lock/unlock feature to access DB
* test: use async version of busted
  And disable benchmark tests (fix later)
* test: add tests for file_lock
* chore: use more explicit names
* chore: use plenary.log instead
* fix: wait async functions definitely
* feat: add migrator
* chore: fix logging
* fix: detect emptiness of the table
* fix: deal with buffer with no names
* test: loosen the condition temporarily
* test: add tests for migrator
* fix: return true when the table is not empty
* feat: load sqlite lazily not to require in start
* chore: add logging to calculate time for fetching
* feat: add converter from native code to SQLite
* feat: warn when sqlite.lua is not available
* feat: add FrecencyMigrateDB to migrate DB
* docs: add note for native code logic
* test: ignore type bug
  • Loading branch information
delphinus authored Aug 27, 2023
1 parent 5d1a01b commit 9037d69
Show file tree
Hide file tree
Showing 18 changed files with 1,268 additions and 411 deletions.
1 change: 1 addition & 0 deletions .luarc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"diagnostics": {
"globals": [
"a",
"describe",
"it",
"vim"
Expand Down
49 changes: 33 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The score is calculated using the age of the 10 most recent timestamps and the t

### Score calculation

```
```lua
score = frequency * recency_score / max_number_of_timestamps
```
## What about files that are neither 'frequent' _or_ 'recent' ?
Expand Down Expand Up @@ -59,9 +59,11 @@ If the active buffer (prior to the finder being launched) is attached to an LSP
## Requirements

- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) (required)
- [sqlite.lua](https://github.com/kkharji/sqlite.lua) (required)
- [sqlite.lua][] (required)
- [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons) (optional)

[sqlite.lua]: https://github.com/kkharji/sqlite.lua

Timestamps and file records are stored in an [SQLite3](https://www.sqlite.org/index.html) database for persistence and speed.
This plugin uses `sqlite.lua` to perform the database transactions.

Expand All @@ -73,9 +75,9 @@ This plugin uses `sqlite.lua` to perform the database transactions.
use {
"nvim-telescope/telescope-frecency.nvim",
config = function()
require"telescope".load_extension("frecency")
require("telescope").load_extension "frecency"
end,
requires = {"kkharji/sqlite.lua"}
requires = { "kkharji/sqlite.lua" },
}
```

Expand All @@ -85,9 +87,9 @@ use {
{
"nvim-telescope/telescope-frecency.nvim",
config = function()
require"telescope".load_extension("frecency")
require("telescope").load_extension "frecency"
end,
dependencies = {"kkharji/sqlite.lua"}
dependencies = { "kkharji/sqlite.lua" },
}
```

Expand All @@ -102,7 +104,7 @@ If no database is found when running Neovim with the plugin installed, a new one
or to map to a key:

```lua
vim.api.nvim_set_keymap("n", "<leader><leader>", "<Cmd>lua require('telescope').extensions.frecency.frecency()<CR>", {noremap = true, silent = true})
vim.api.nvim_set_keymap("n", "<leader><leader>", "<Cmd>Telescope frecency<CR>")
```

Use a specific workspace tag:
Expand All @@ -114,7 +116,7 @@ Use a specific workspace tag:
or

```lua
vim.api.nvim_set_keymap("n", "<leader><leader>", "<Cmd>lua require('telescope').extensions.frecency.frecency({ workspace = 'CWD' })<CR>", {noremap = true, silent = true})
vim.api.nvim_set_keymap("n", "<leader><leader>", "<Cmd>Telescope frecency workspace=CWD<CR>")
```

Filter tags are applied by typing the `:tag:` name (adding surrounding colons) in the finder query.
Expand All @@ -124,7 +126,7 @@ Entering `:<Tab>` will trigger omnicompletion for available tags.

See [default configuration](https://github.com/nvim-telescope/telescope.nvim#telescope-defaults) for full details on configuring Telescope.

- `db_root` (default: `nil`)
- `db_root` (default: `vim.fn.stdpath "data"`)

Path to parent directory of custom database location.
Defaults to `$XDG_DATA_HOME/nvim` if unset.
Expand All @@ -133,15 +135,15 @@ See [default configuration](https://github.com/nvim-telescope/telescope.nvim#tel

Default workspace tag to filter by e.g. `'CWD'` to filter by default to the current directory. Can be overridden at query time by specifying another filter like `':*:'`.

- `ignore_patterns` (default: `{"*.git/*", "*/tmp/*"}`)
- `ignore_patterns` (default: `{ "*.git/*", "*/tmp/*", "term://*" }`)

Patterns in this table control which files are indexed (and subsequently which you'll see in the finder results).

- `show_scores` (default : `false`)

To see the scores generated by the algorithm in the results, set this to `true`.

- `workspaces` (default: {})
- `workspaces` (default: `{}`)

This table contains mappings of `workspace_tag` -> `workspace_directory`
The key corresponds to the `:tag_name` used to select the filter in queries.
Expand All @@ -164,10 +166,13 @@ See [default configuration](https://github.com/nvim-telescope/telescope.nvim#tel
show_filter_column = { "LSP", "CWD", "FOO" }
```

- `use_sqlite` (default: `true`) ***experimental feature***

Use [sqlite.lua] `true` or native code `false`. See [*Remove dependency for sqlite.lua*](#remove-dependency-for-sqlite.lua) for the detail.

### Example Configuration:

```
```lua
telescope.setup {
extensions = {
frecency = {
Expand All @@ -187,12 +192,14 @@ telescope.setup {
}
```

### SQL database location
## Note for Database

The default location for the sqlite3 database is `$XDG_DATA_HOME/nvim` (eg `~/.local/share/nvim/` on linux).
### Location

The default location for the database is `$XDG_DATA_HOME/nvim` (eg `~/.local/share/nvim/` on linux).
This can be configured with the `db_root` config option.

### SQL database maintainance
### Maintainance

By default, frecency will prune files that no longer exist from the database.
In certain workflows, switching branches in a repository, that behaviour might not be desired.
Expand All @@ -210,7 +217,17 @@ The command `FrecencyValidate` can be used to clean the database when `auto_vali
:FrecencyValidate!
```

### Highlight Groups
### Remove dependency for [sqlite.lua][]

***This is an experimental feature.***

In default, it uses SQLite3 library to access the DB. When `use_sqlite` option is set to `false`, it stores the whole data and saves them with encoding by `string.dump()` Lua function.

With this, we can remove the dependency for [sqlite.lua][] and obtain faster speed to open `:Telescope frecency`.

You can migrate from SQLite DB into native code by `:FrecencyMigrateDB` command. It converts data into native code, but does not delete the existent SQLite DB. You can use old SQLite logic by `use_sqlite = true` again.

## Highlight Groups

```vim
TelescopeBufferLoaded
Expand Down
141 changes: 24 additions & 117 deletions lua/frecency/database.lua
Original file line number Diff line number Diff line change
@@ -1,136 +1,43 @@
local sqlite = require "sqlite"
local log = require "plenary.log"

---@diagnostic disable: missing-return, unused-local
---@class FrecencyDatabaseConfig
---@field root string

---@class FrecencySqlite: sqlite_db
---@field files sqlite_tbl
---@field timestamps sqlite_tbl

---@class FrecencyFile
---@field count integer
---@field id integer
---@field path string
---@field score integer calculated from count and age

---@class FrecencyTimestamp
---@field age integer calculated from timestamp
---@field file_id integer
---@field id integer
---@field timestamp number

---@class FrecencyDatabaseGetFilesOptions
---@field path string?
---@field workspace string?

---@class FrecencyDatabase
---@field config FrecencyDatabaseConfig
---@field private buf_registered_flag_name string
---@field private fs FrecencyFS
---@field private sqlite FrecencySqlite
---@field has_entry fun(): boolean
---@field new fun(fs: FrecencyFS, config: FrecencyDatabaseConfig): FrecencyDatabase
---@field protected fs FrecencyFS
local Database = {}

---@param fs FrecencyFS
---@param config FrecencyDatabaseConfig
---@return FrecencyDatabase
Database.new = function(fs, config)
local lib = sqlite.lib --[[@as sqlite_lib]]
local self = setmetatable(
{ config = config, buf_registered_flag_name = "telescope_frecency_registered", fs = fs },
{ __index = Database }
)
self.sqlite = sqlite {
uri = self.config.root .. "/file_frecency.sqlite3",
files = { id = true, count = { "integer", default = 1, required = true }, path = "string" },
timestamps = {
id = true,
file_id = { "integer", reference = "files.id", on_delete = "cascade" },
timestamp = { "real", default = lib.julianday "now" },
},
}
return self
end

---@return boolean
function Database:has_entry()
return self.sqlite.files:count() > 0
end

---@param paths string[]
---@return integer
function Database:insert_files(paths)
if #paths == 0 then
return 0
end
---@param path string
return self.sqlite.files:insert(vim.tbl_map(function(path)
return { path = path, count = 0 } -- TODO: remove when sql.nvim#97 is closed
end, paths))
end
---@return nil
function Database:insert_files(paths) end

---@param workspace string?
---@return FrecencyFile[]
function Database:get_files(workspace)
local query = workspace and { contains = { path = { workspace .. "/*" } } } or {}
log.debug { query = query }
return self.sqlite.files:get(query)
end
---@return integer[]|string[]
function Database:unlinked_entries() end

---@param datetime string? ISO8601 format string
---@return FrecencyTimestamp[]
function Database:get_timestamps(datetime)
local lib = sqlite.lib
local age = lib.cast((lib.julianday(datetime) - lib.julianday "timestamp") * 24 * 60, "integer")
return self.sqlite.timestamps:get { keys = { age = age, "id", "file_id" } }
end
---@param files integer[]|string[]
---@return nil
function Database:remove_files(files) end

---@param path string
---@return integer: id of the file entry
---@return boolean: whether the entry is inserted (true) or updated (false)
function Database:upsert_files(path)
local file = self.sqlite.files:get({ where = { path = path } })[1] --[[@as FrecencyFile?]]
if file then
self.sqlite.files:update { where = { id = file.id }, set = { count = file.count + 1 } }
return file.id, false
end
return self.sqlite.files:insert { path = path }, true
end

---@param file_id integer
---@param datetime string? ISO8601 format string
---@return integer
function Database:insert_timestamps(file_id, datetime)
return self.sqlite.timestamps:insert {
file_id = file_id,
timestamp = datetime and sqlite.lib.julianday(datetime) or nil,
}
end

---@param file_id integer
---@param max_count integer
function Database:trim_timestamps(file_id, max_count)
local timestamps = self.sqlite.timestamps:get { where = { file_id = file_id } } --[[@as FrecencyTimestamp[] ]]
local trim_at = timestamps[#timestamps - max_count + 1]
if trim_at then
self.sqlite.timestamps:remove { file_id = tostring(file_id), id = "<" .. tostring(trim_at.id) }
end
end

---@return integer[]
function Database:unlinked_entries()
---@param file FrecencyFile
return self.sqlite.files:map(function(file)
if not self.fs:is_valid_path(file.path) then
return file.id
end
end)
end

---@param ids integer[]
---@param datetime string?
---@return nil
function Database:remove_files(ids)
self.sqlite.files:remove { id = ids }
end
function Database:update(path, max_count, datetime) end

return Database
---@async
---@class FrecencyDatabaseEntry
---@field ages number[]
---@field count integer
---@field path string
---@field score number

---@param workspace string?
---@param datetime string?
---@return FrecencyDatabaseEntry[]
function Database:get_entries(workspace, datetime) end
Loading

0 comments on commit 9037d69

Please sign in to comment.