nanasqlite 1.3.3.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ """
2
+ NanaSQLite Exception Classes
3
+
4
+ カスタム例外クラスを定義し、エラーハンドリングを統一する。
5
+ """
6
+
7
+
8
+ class NanaSQLiteError(Exception):
9
+ """
10
+ NanaSQLiteの基底例外クラス
11
+
12
+ すべてのNanaSQLite固有の例外はこのクラスを継承する。
13
+ """
14
+
15
+ pass
16
+
17
+
18
+ class NanaSQLiteValidationError(NanaSQLiteError):
19
+ """
20
+ バリデーションエラー
21
+
22
+ 不正な入力値やパラメータに対して発生する。
23
+
24
+ Examples:
25
+ - 不正なテーブル名やカラム名
26
+ - 不正なSQL識別子
27
+ - パラメータの型エラー
28
+ """
29
+
30
+ pass
31
+
32
+
33
+ class NanaSQLiteDatabaseError(NanaSQLiteError):
34
+ """
35
+ データベース操作エラー
36
+
37
+ SQLite/APSWのデータベース操作で発生するエラーをラップ。
38
+
39
+ Examples:
40
+ - データベースロック
41
+ - ディスク容量不足
42
+ - ファイル権限エラー
43
+ - SQL構文エラー
44
+ """
45
+
46
+ def __init__(self, message: str, original_error: Exception = None):
47
+ super().__init__(message)
48
+ self.original_error = original_error
49
+
50
+
51
+ class NanaSQLiteTransactionError(NanaSQLiteError):
52
+ """
53
+ トランザクション関連エラー
54
+
55
+ トランザクションの開始、コミット、ロールバックで発生するエラー。
56
+
57
+ Examples:
58
+ - ネストしたトランザクションの試み
59
+ - トランザクション外でのコミット/ロールバック
60
+ - トランザクション中の接続クローズ
61
+ """
62
+
63
+ pass
64
+
65
+
66
+ class NanaSQLiteConnectionError(NanaSQLiteError):
67
+ """
68
+ 接続エラー
69
+
70
+ データベース接続の作成や管理で発生するエラー。
71
+
72
+ Examples:
73
+ - 閉じられた接続の使用
74
+ - 接続の初期化失敗
75
+ - 孤立した子インスタンスの使用
76
+ """
77
+
78
+ pass
79
+
80
+
81
+ class NanaSQLiteClosedError(NanaSQLiteConnectionError):
82
+ """
83
+ Closed instance error / クローズ済みエラー
84
+
85
+ Occurs when operating on a closed instance, or on a child instance whose parent has been closed.
86
+ 閉じられたインスタンスや、親が閉じられた子インスタンスを操作しようとした時に発生。
87
+ """
88
+
89
+ pass
90
+
91
+
92
+ class NanaSQLiteLockError(NanaSQLiteError):
93
+ """
94
+ ロック取得エラー
95
+
96
+ データベースロックの取得に失敗した場合に発生。
97
+
98
+ Examples:
99
+ - ロック取得タイムアウト
100
+ - デッドロック検出
101
+ """
102
+
103
+ pass
104
+
105
+
106
+ class NanaSQLiteCacheError(NanaSQLiteError):
107
+ """
108
+ キャッシュ関連エラー
109
+
110
+ キャッシュの操作で発生するエラー。
111
+
112
+ Examples:
113
+ - キャッシュサイズ超過
114
+ - キャッシュの不整合
115
+ """
116
+
117
+ pass
nanasqlite/py.typed ADDED
File without changes
@@ -0,0 +1,174 @@
1
+ """
2
+ SQL utility functions for NanaSQLite.
3
+
4
+ This module provides utility functions for SQL string processing,
5
+ particularly for sanitizing SQL expressions to prevent injection attacks
6
+ and handle edge cases in SQL parsing.
7
+ """
8
+
9
+
10
+ def sanitize_sql_for_function_scan(sql: str) -> str:
11
+ """
12
+ Return a version of the SQL string where string literals and comments
13
+ are replaced with spaces so that function-like patterns inside them
14
+ are ignored by the validation regex.
15
+
16
+ This function implements a state machine that processes SQL character by character,
17
+ tracking whether we're inside:
18
+ - Single-quoted string literals (with '' escaping)
19
+ - Double-quoted identifiers/strings (with "" escaping)
20
+ - Line comments (-- to newline)
21
+ - Block comments (/* to */)
22
+
23
+ Args:
24
+ sql: The SQL string to sanitize
25
+
26
+ Returns:
27
+ A sanitized version of the SQL string where all content inside
28
+ string literals and comments is replaced with spaces, while
29
+ preserving the original length and newline positions.
30
+
31
+ Example:
32
+ >>> sanitize_sql_for_function_scan("SELECT 'COUNT(*)' FROM table")
33
+ 'SELECT FROM table'
34
+ >>> sanitize_sql_for_function_scan("SELECT COUNT(*) -- comment")
35
+ 'SELECT COUNT(*) '
36
+
37
+ Note:
38
+ This function handles SQL-specific escaping rules:
39
+ - Single quotes are escaped as ''
40
+ - Double quotes are escaped as ""
41
+ - Line comments start with -- and end at newline
42
+ - Block comments are /* ... */
43
+ """
44
+ if not sql:
45
+ return sql
46
+
47
+ result = []
48
+ i = 0
49
+ length = len(sql)
50
+ in_single = False
51
+ in_double = False
52
+ in_line_comment = False
53
+ in_block_comment = False
54
+
55
+ while i < length:
56
+ ch = sql[i]
57
+
58
+ # Inside line comment
59
+ if in_line_comment:
60
+ if ch == "\n":
61
+ in_line_comment = False
62
+ result.append(ch)
63
+ else:
64
+ result.append(" ")
65
+ i += 1
66
+ continue
67
+
68
+ # Inside block comment
69
+ if in_block_comment:
70
+ if ch == "*" and i + 1 < length and sql[i + 1] == "/":
71
+ in_block_comment = False
72
+ result.append(" ") # Replace */
73
+ i += 2
74
+ else:
75
+ result.append(" ")
76
+ i += 1
77
+ continue
78
+
79
+ # Inside single-quoted string literal
80
+ if in_single:
81
+ if ch == "'" and i + 1 < length and sql[i + 1] == "'":
82
+ # Escaped single quote (SQL standard: '')
83
+ result.append(" ")
84
+ i += 2
85
+ elif ch == "'":
86
+ in_single = False
87
+ result.append(" ")
88
+ i += 1
89
+ else:
90
+ result.append(" ")
91
+ i += 1
92
+ continue
93
+
94
+ # Inside double-quoted identifier/string
95
+ if in_double:
96
+ if ch == '"' and i + 1 < length and sql[i + 1] == '"':
97
+ # Escaped double quote (SQL standard: "")
98
+ result.append(" ")
99
+ i += 2
100
+ elif ch == '"':
101
+ in_double = False
102
+ result.append(" ")
103
+ i += 1
104
+ else:
105
+ result.append(" ")
106
+ i += 1
107
+ continue
108
+
109
+ # Outside literals/comments - check for delimiters
110
+
111
+ # Line comment start
112
+ if ch == "-" and i + 1 < length and sql[i + 1] == "-":
113
+ in_line_comment = True
114
+ result.append(" ")
115
+ i += 2
116
+ continue
117
+
118
+ # Block comment start
119
+ if ch == "/" and i + 1 < length and sql[i + 1] == "*":
120
+ in_block_comment = True
121
+ result.append(" ")
122
+ i += 2
123
+ continue
124
+
125
+ # Single-quoted string start
126
+ if ch == "'":
127
+ in_single = True
128
+ result.append(" ")
129
+ i += 1
130
+ continue
131
+
132
+ # Double-quoted identifier start
133
+ if ch == '"':
134
+ in_double = True
135
+ result.append(" ")
136
+ i += 1
137
+ continue
138
+
139
+ # Normal code - preserve as-is
140
+ result.append(ch)
141
+ i += 1
142
+
143
+ return "".join(result)
144
+
145
+
146
+ def fast_validate_sql_chars(expr: str) -> bool:
147
+ """
148
+ Validate that a SQL expression contains only safe characters.
149
+ This is a ReDoS-resistant alternative to complex regex for basic validation.
150
+
151
+ Safe characters include:
152
+ - Alphanumeric characters
153
+ - Underscore (_)
154
+ - Space ( )
155
+ - Comma (,) -- for ORDER BY/GROUP BY
156
+ - Dot (.) -- for table.column
157
+ - Parentheses (()) -- for function calls
158
+ - Operators: =, <, >, !, +, -, *, /
159
+ - Quotes: ', " (handled carefully by other layers)
160
+
161
+ Args:
162
+ expr: The SQL expression to validate
163
+
164
+ Returns:
165
+ True if all characters are within the safe set, False otherwise.
166
+ """
167
+ if not expr:
168
+ return True
169
+
170
+ # Safe character set: Alphanumeric, underscores, spaces, and common SQL punctuation/operators
171
+ # Including ?, :, @, $ for parameter placeholders
172
+ safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ ,.()'=<>!+-*/\"|?:@$")
173
+
174
+ return all(c in safe_chars for c in expr)
nanasqlite/utils.py ADDED
@@ -0,0 +1,202 @@
1
+ """
2
+ Utility module for NanaSQLite: Provides ExpiringDict and other helper classes.
3
+ (NanaSQLite用ユーティリティモジュール: 有効期限付き辞書やその他の補助クラスを提供)
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import collections.abc
10
+ import logging
11
+ import threading
12
+ import time
13
+ from collections.abc import Iterator
14
+ from enum import Enum
15
+ from typing import Any, Callable
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ExpirationMode(str, Enum):
21
+ """
22
+ Modes for detecting and handling expired items.
23
+ (有効期限切れの検知と処理モード)
24
+ """
25
+
26
+ LAZY = "lazy" # Check on access only (アクセ時にのみチェック)
27
+ SCHEDULER = "scheduler" # Single background worker (単一のバックグラウンドワーカー)
28
+ TIMER = "timer" # Individual timers for each key (キーごとの個別タイマー)
29
+
30
+
31
+ class ExpiringDict(collections.abc.MutableMapping):
32
+ """
33
+ A dictionary-like object where keys expire after a set time.
34
+ Supports multiple expiration modes for different scales and precision requirements.
35
+ (キーが一定時間後に失効する辞書型オブジェクト。規模や精度に応じて複数の失効モードをサポート)
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ expiration_time: float,
41
+ mode: ExpirationMode = ExpirationMode.SCHEDULER,
42
+ on_expire: Callable[[str, Any], None] | None = None,
43
+ ):
44
+ """
45
+ Args:
46
+ expiration_time: Time in seconds before a key expires. (失効までの時間(秒))
47
+ mode: Expiration detection mode. (失効検知モード)
48
+ on_expire: Callback function called when an item expires. (失効時に呼ばれるコールバック)
49
+ """
50
+ self._data: dict[str, Any] = {}
51
+ self._exptimes: dict[str, float] = {} # key -> expiry (Unix timestamp)
52
+ self._expiration_time = expiration_time
53
+ self._mode = mode
54
+ self._on_expire = on_expire
55
+
56
+ # Mode specific structures
57
+ self._timers: dict[str, threading.Timer] = {}
58
+ self._async_tasks: dict[str, asyncio.Task] = {}
59
+ self._scheduler_thread: threading.Thread | None = None
60
+ self._scheduler_running = False
61
+ self._lock = threading.RLock()
62
+
63
+ if self._mode == ExpirationMode.SCHEDULER:
64
+ self._start_scheduler()
65
+
66
+ def _start_scheduler(self) -> None:
67
+ """Start the background scheduler thread if not already running."""
68
+ with self._lock:
69
+ if self._scheduler_running:
70
+ return
71
+ self._scheduler_running = True
72
+ self._scheduler_thread = threading.Thread(target=self._scheduler_loop, daemon=True)
73
+ self._scheduler_thread.start()
74
+
75
+ def _scheduler_loop(self) -> None:
76
+ """Single background worker loop to evict expired items."""
77
+ while self._scheduler_running:
78
+ now = time.time()
79
+ expired_keys = []
80
+
81
+ with self._lock:
82
+ # Since items are added in order, the first item is the oldest (likely to expire first)
83
+ if not self._exptimes:
84
+ sleep_time = 1.0
85
+ else:
86
+ first_key = next(iter(self._exptimes))
87
+ expiry = self._exptimes[first_key]
88
+ if expiry <= now:
89
+ expired_keys.append(first_key)
90
+ sleep_time = 0 # Process next immediately
91
+ else:
92
+ sleep_time = min(expiry - now, 1.0)
93
+
94
+ # Do deletion outside of the common lock if possible, but for simplicity here we keep it safe
95
+ for key in expired_keys:
96
+ self._evict(key)
97
+
98
+ if sleep_time > 0:
99
+ time.sleep(sleep_time)
100
+
101
+ def _evict(self, key: str) -> None:
102
+ """Evict an item and trigger callback."""
103
+ with self._lock:
104
+ if key in self._data:
105
+ value = self._data.pop(key)
106
+ self._exptimes.pop(key, None)
107
+ if self._on_expire:
108
+ try:
109
+ self._on_expire(key, value)
110
+ except Exception as e:
111
+ logger.error(f"Error in ExpiringDict on_expire callback for key '{key}': {e}")
112
+ logger.debug(f"Key '{key}' expired and removed.")
113
+
114
+ def _check_expiry(self, key: str) -> bool:
115
+ """Check if a key is expired and remove it if it is (Lazy eviction)."""
116
+ now = time.time()
117
+ with self._lock:
118
+ if key in self._exptimes and self._exptimes[key] <= now:
119
+ self._evict(key)
120
+ return True
121
+ return False
122
+
123
+ def __setitem__(self, key: str, value: Any) -> None:
124
+ expiry = time.time() + self._expiration_time
125
+ with self._lock:
126
+ # If item already exists, remove it first to maintain insertion order (for FIFO/Scheduler)
127
+ if key in self._data:
128
+ self._cancel_timer(key)
129
+ del self._data[key]
130
+ del self._exptimes[key]
131
+
132
+ self._data[key] = value
133
+ self._exptimes[key] = expiry
134
+
135
+ if self._mode == ExpirationMode.TIMER:
136
+ self._set_timer(key)
137
+
138
+ def _set_timer(self, key: str) -> None:
139
+ """Set individual timer (TIMER mode)."""
140
+ # Try to use current event loop if available, else use threading.Timer
141
+ try:
142
+ loop = asyncio.get_running_loop()
143
+ task = loop.call_later(self._expiration_time, self._evict, key)
144
+ self._async_tasks[key] = task # type: ignore
145
+ except RuntimeError:
146
+ timer = threading.Timer(self._expiration_time, self._evict, args=(key,))
147
+ timer.daemon = True
148
+ timer.start()
149
+ self._timers[key] = timer
150
+
151
+ def _cancel_timer(self, key: str) -> None:
152
+ """Cancel individual timer (TIMER mode)."""
153
+ if key in self._timers:
154
+ self._timers[key].cancel()
155
+ del self._timers[key]
156
+ if key in self._async_tasks:
157
+ self._async_tasks[key].cancel()
158
+ del self._async_tasks[key]
159
+
160
+ def __getitem__(self, key: str) -> Any:
161
+ self._check_expiry(key)
162
+ with self._lock:
163
+ return self._data[key]
164
+
165
+ def __delitem__(self, key: str) -> None:
166
+ with self._lock:
167
+ self._cancel_timer(key)
168
+ if key in self._data:
169
+ del self._data[key]
170
+ del self._exptimes[key]
171
+
172
+ def __iter__(self) -> Iterator[str]:
173
+ # Clean up expired items during iteration to stay accurate
174
+ keys = list(self._data.keys())
175
+ for key in keys:
176
+ if not self._check_expiry(key):
177
+ yield key
178
+
179
+ def __len__(self) -> int:
180
+ # Note: could be inaccurate if items expired but not yet evicted
181
+ # But for performance we return current size.
182
+ return len(self._data)
183
+
184
+ def __contains__(self, key: object) -> bool:
185
+ if not isinstance(key, str):
186
+ return False
187
+ return not self._check_expiry(key) and key in self._data
188
+
189
+ def set_on_expire_callback(self, callback: Callable[[str, Any], None]) -> None:
190
+ """Update the expiration callback."""
191
+ self._on_expire = callback
192
+
193
+ def clear(self) -> None:
194
+ with self._lock:
195
+ for key in list(self._timers.keys()):
196
+ self._cancel_timer(key)
197
+ self._data.clear()
198
+ self._exptimes.clear()
199
+ self._scheduler_running = False
200
+
201
+ def __del__(self):
202
+ self._scheduler_running = False