Skip to content

Commit 5b2e84c

Browse files
author
Developer
committed
feat: 基于使用频率的智能SQL补全排序功能(合并提交)
1 parent d0a6cc2 commit 5b2e84c

5 files changed

Lines changed: 217 additions & 5 deletions

File tree

pgcli/key_bindings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def pgcli_bindings(pgcli):
2424
def _(event):
2525
"""Enable/Disable SmartCompletion Mode."""
2626
_logger.debug("Detected F2 key.")
27-
pgcli.completer.smart_completion = not pgcli.completer.smart_completion
27+
new_state = not pgcli.completer.smart_completion
28+
pgcli.completer.set_smart_completion(new_state)
2829

2930
@kb.add("f3")
3031
def _(event):

pgcli/packages/history_freq.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import sqlite3
2+
import os
3+
import platform
4+
import re
5+
import sqlparse
6+
from sqlparse.tokens import Name
7+
from os.path import expanduser
8+
from .pgliterals.main import get_literals
9+
10+
11+
white_space_regex = re.compile("\\s+", re.MULTILINE)
12+
13+
14+
def _compile_regex(keyword):
15+
# Surround the keyword with word boundaries and replace interior whitespace
16+
# with whitespace wildcards
17+
pattern = "\\b" + white_space_regex.sub(r"\\s+", keyword) + "\\b"
18+
return re.compile(pattern, re.MULTILINE | re.IGNORECASE)
19+
20+
21+
keywords = get_literals("keywords")
22+
keyword_regexs = {kw: _compile_regex(kw) for kw in keywords}
23+
24+
25+
def history_freq_location():
26+
"""Return the path to the history frequency database location."""
27+
if "XDG_DATA_HOME" in os.environ:
28+
return "%s/pgcli/history_freq.db" % expanduser(os.environ["XDG_DATA_HOME"])
29+
elif platform.system() == "Windows":
30+
return os.getenv("USERPROFILE") + "\\AppData\\Local\\dbcli\\pgcli\\history_freq.db"
31+
else:
32+
return expanduser("~/.local/share/pgcli/history_freq.db")
33+
34+
35+
class HistoryFrequency:
36+
def __init__(self, db_path=None):
37+
"""Initialize the history frequency tracker.
38+
:param db_path: path to the SQLite database file.
39+
"""
40+
self.db_path = db_path or history_freq_location()
41+
self.conn = None
42+
43+
# For in-memory databases, we need to keep the connection open
44+
if self.db_path == ":memory:":
45+
self.conn = sqlite3.connect(self.db_path)
46+
self._create_table(self.conn)
47+
else:
48+
self._create_table()
49+
50+
def _create_table(self, conn=None):
51+
"""Create the frequency tables if they don't exist."""
52+
# Ensure directory exists (skip for in-memory databases)
53+
if self.db_path != ":memory:":
54+
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
55+
56+
if conn is None:
57+
conn = sqlite3.connect(self.db_path)
58+
with conn:
59+
self._create_tables_on_conn(conn)
60+
conn.close()
61+
else:
62+
self._create_tables_on_conn(conn)
63+
64+
def _create_tables_on_conn(self, conn):
65+
"""Create tables on the given connection."""
66+
conn.execute("""
67+
CREATE TABLE IF NOT EXISTS keyword_frequency (
68+
keyword TEXT PRIMARY KEY,
69+
frequency INTEGER DEFAULT 1,
70+
last_used TIMESTAMP DEFAULT CURRENT_TIMESTAMP
71+
)
72+
""")
73+
conn.execute("""
74+
CREATE TABLE IF NOT EXISTS name_frequency (
75+
name TEXT PRIMARY KEY,
76+
frequency INTEGER DEFAULT 1,
77+
last_used TIMESTAMP DEFAULT CURRENT_TIMESTAMP
78+
)
79+
""")
80+
81+
def _get_connection(self):
82+
"""Get a database connection."""
83+
if self.db_path == ":memory:":
84+
return self.conn
85+
return sqlite3.connect(self.db_path)
86+
87+
def _close_connection(self, conn):
88+
"""Close the connection unless it's an in-memory database."""
89+
if self.db_path != ":memory:":
90+
conn.close()
91+
92+
def increment_keyword(self, keyword):
93+
"""Increment the frequency count for a keyword."""
94+
keyword = keyword.lower()
95+
conn = self._get_connection()
96+
with conn:
97+
conn.execute("""
98+
INSERT OR REPLACE INTO keyword_frequency (keyword, frequency, last_used)
99+
VALUES (
100+
?,
101+
COALESCE((SELECT frequency FROM keyword_frequency WHERE keyword = ?), 0) + 1,
102+
CURRENT_TIMESTAMP
103+
)
104+
""", (keyword, keyword))
105+
self._close_connection(conn)
106+
107+
def increment_name(self, name):
108+
"""Increment the frequency count for a name/identifier."""
109+
name = name.lower()
110+
conn = self._get_connection()
111+
with conn:
112+
conn.execute("""
113+
INSERT OR REPLACE INTO name_frequency (name, frequency, last_used)
114+
VALUES (
115+
?,
116+
COALESCE((SELECT frequency FROM name_frequency WHERE name = ?), 0) + 1,
117+
CURRENT_TIMESTAMP
118+
)
119+
""", (name, name))
120+
self._close_connection(conn)
121+
122+
def get_keyword_frequency(self, keyword):
123+
"""Get the frequency count for a keyword."""
124+
keyword = keyword.lower()
125+
conn = self._get_connection()
126+
cursor = conn.cursor()
127+
cursor.execute("SELECT frequency FROM keyword_frequency WHERE keyword = ?", (keyword,))
128+
result = cursor.fetchone()
129+
self._close_connection(conn)
130+
return result[0] if result else 0
131+
132+
def get_name_frequency(self, name):
133+
"""Get the frequency count for a name/identifier."""
134+
name = name.lower()
135+
conn = self._get_connection()
136+
cursor = conn.cursor()
137+
cursor.execute("SELECT frequency FROM name_frequency WHERE name = ?", (name,))
138+
result = cursor.fetchone()
139+
self._close_connection(conn)
140+
return result[0] if result else 0
141+
142+
def update_from_text(self, text):
143+
"""Update frequencies from SQL text by extracting keywords and names."""
144+
self.update_keywords(text)
145+
self.update_names(text)
146+
147+
def update_keywords(self, text):
148+
"""Update keyword frequencies from SQL text."""
149+
for keyword, regex in keyword_regexs.items():
150+
count = len(list(regex.finditer(text)))
151+
if count > 0:
152+
keyword_lower = keyword.lower()
153+
conn = self._get_connection()
154+
with conn:
155+
conn.execute("""
156+
INSERT OR REPLACE INTO keyword_frequency (keyword, frequency, last_used)
157+
VALUES (
158+
?,
159+
COALESCE((SELECT frequency FROM keyword_frequency WHERE keyword = ?), 0) + ?,
160+
CURRENT_TIMESTAMP
161+
)
162+
""", (keyword_lower, keyword_lower, count))
163+
self._close_connection(conn)
164+
165+
def update_names(self, text):
166+
"""Update name/identifier frequencies from SQL text."""
167+
for parsed in sqlparse.parse(text):
168+
for token in parsed.flatten():
169+
if token.ttype in Name:
170+
name_lower = token.value.lower()
171+
conn = self._get_connection()
172+
with conn:
173+
conn.execute("""
174+
INSERT OR REPLACE INTO name_frequency (name, frequency, last_used)
175+
VALUES (
176+
?,
177+
COALESCE((SELECT frequency FROM name_frequency WHERE name = ?), 0) + 1,
178+
CURRENT_TIMESTAMP
179+
)
180+
""", (name_lower, name_lower))
181+
self._close_connection(conn)
182+
183+
def clear(self):
184+
"""Clear all frequency data."""
185+
conn = self._get_connection()
186+
with conn:
187+
conn.execute("DELETE FROM keyword_frequency")
188+
conn.execute("DELETE FROM name_frequency")
189+
self._close_connection(conn)

pgcli/packages/prioritization.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from sqlparse.tokens import Name
44
from collections import defaultdict
55
from .pgliterals.main import get_literals
6+
from .history_freq import HistoryFrequency
67

78

89
white_space_regex = re.compile("\\s+", re.MULTILINE)
@@ -20,9 +21,11 @@ def _compile_regex(keyword):
2021

2122

2223
class PrevalenceCounter:
23-
def __init__(self):
24+
def __init__(self, use_history=False):
2425
self.keyword_counts = defaultdict(int)
2526
self.name_counts = defaultdict(int)
27+
self.use_history = use_history
28+
self.history_freq = HistoryFrequency() if use_history else None
2629

2730
def update(self, text):
2831
self.update_keywords(text)
@@ -32,7 +35,10 @@ def update_names(self, text):
3235
for parsed in sqlparse.parse(text):
3336
for token in parsed.flatten():
3437
if token.ttype in Name:
35-
self.name_counts[token.value] += 1
38+
value = token.value
39+
self.name_counts[value] += 1
40+
if self.use_history and self.history_freq:
41+
self.history_freq.increment_name(value)
3642

3743
def clear_names(self):
3844
self.name_counts = defaultdict(int)
@@ -43,9 +49,15 @@ def update_keywords(self, text):
4349
for keyword, regex in keyword_regexs.items():
4450
for _ in regex.finditer(text):
4551
self.keyword_counts[keyword] += 1
52+
if self.use_history and self.history_freq:
53+
self.history_freq.increment_keyword(keyword)
4654

4755
def keyword_count(self, keyword):
56+
if self.use_history and self.history_freq:
57+
return self.keyword_counts[keyword] + self.history_freq.get_keyword_frequency(keyword)
4858
return self.keyword_counts[keyword]
4959

5060
def name_count(self, name):
61+
if self.use_history and self.history_freq:
62+
return self.name_counts[name] + self.history_freq.get_name_frequency(name)
5163
return self.name_counts[name]

pgcli/pgclirc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
# Enables context sensitive auto-completion. If this is disabled, all
55
# possible completions will be listed.
6-
smart_completion = True
6+
# Also enables history-based frequency sorting when enabled (uses SQLite database
7+
# to persist completion frequency data across sessions in ~/.config/pgcli/history_freq.db)
8+
smart_completion = False
79

810
# Display the completions in several columns. (More completions will be
911
# visible.)

pgcli/pgcompleter.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ def __init__(self, smart_completion=True, pgspecial=None, settings=None):
106106
super().__init__()
107107
self.smart_completion = smart_completion
108108
self.pgspecial = pgspecial
109-
self.prioritizer = PrevalenceCounter()
110109
settings = settings or {}
110+
# Use history-based frequency sorting when smart_completion is enabled
111+
# History frequency data is persisted across sessions in ~/.config/pgcli/history_freq.db
112+
self.prioritizer = PrevalenceCounter(use_history=self.smart_completion)
111113
self.signature_arg_style = settings.get("signature_arg_style", "{arg_name} {arg_type}")
112114
self.call_arg_style = settings.get("call_arg_style", "{arg_name: <{max_arg_len}} := {arg_default}")
113115
self.call_arg_display_style = settings.get("call_arg_display_style", "{arg_name}")
@@ -307,6 +309,12 @@ def extend_query_history(self, text, is_init=False):
307309
def set_search_path(self, search_path):
308310
self.search_path = self.escaped_names(search_path)
309311

312+
def set_smart_completion(self, enabled):
313+
"""Enable or disable smart completion (including history-based sorting)."""
314+
self.smart_completion = enabled
315+
# Recreate prioritizer with the new setting
316+
self.prioritizer = PrevalenceCounter(use_history=self.smart_completion)
317+
310318
def reset_completions(self):
311319
self.databases = []
312320
self.special_commands = []

0 commit comments

Comments
 (0)