minidb-sql 1.3.0__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.
minidb_sql/table.py ADDED
@@ -0,0 +1,274 @@
1
+ """Table storage and row management for MiniDB."""
2
+
3
+ from collections.abc import Iterable, Iterator
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from .column import Schema
8
+ from .errors import ColumnNotFoundError, DuplicateKeyError
9
+ from .index import IndexManager
10
+ from .types import Row
11
+
12
+
13
+ @dataclass
14
+ class Table:
15
+ """
16
+ Represents a database table with rows and a schema.
17
+
18
+ Stores rows as dictionaries and maintains indexes for fast lookups.
19
+ """
20
+
21
+ name: str
22
+ schema: Schema
23
+ _rows: list[Row] = field(default_factory=list)
24
+ _row_ids: list[int] = field(default_factory=list)
25
+ _next_row_id: int = field(default=0)
26
+ _index_manager: IndexManager = field(default_factory=IndexManager)
27
+
28
+ def __post_init__(self):
29
+ """Initialize indexes after table creation."""
30
+ # Create index on primary key if exists
31
+ if self.schema.primary_key:
32
+ self._index_manager.create_index(self.schema.primary_key)
33
+
34
+ @property
35
+ def row_count(self) -> int:
36
+ """Get the number of rows in the table."""
37
+ return len(self._rows)
38
+
39
+ @property
40
+ def columns(self) -> list[str]:
41
+ """Get list of column names."""
42
+ return self.schema.column_names
43
+
44
+ @property
45
+ def primary_key(self) -> str | None:
46
+ """Get the primary key column name."""
47
+ return self.schema.primary_key
48
+
49
+ def insert(self, row: dict) -> int:
50
+ """
51
+ Insert a new row into the table.
52
+
53
+ Args:
54
+ row: Row data as a dictionary
55
+
56
+ Returns:
57
+ The row ID of the inserted row
58
+
59
+ Raises:
60
+ DuplicateKeyError: If primary key already exists
61
+ """
62
+ # Validate and normalize the row
63
+ validated_row = self.schema.validate_row(row, allow_missing=True)
64
+
65
+ # Check for duplicate primary key
66
+ pk_column = self.schema.primary_key
67
+ if pk_column:
68
+ pk_value = validated_row[pk_column]
69
+ if pk_value is not None:
70
+ index = self._index_manager.get_index(pk_column)
71
+ if index and index.has_value(pk_value):
72
+ raise DuplicateKeyError(pk_value)
73
+
74
+ # Assign row ID
75
+ row_id = self._next_row_id
76
+ self._next_row_id += 1
77
+
78
+ # Store the row
79
+ self._rows.append(validated_row)
80
+ self._row_ids.append(row_id)
81
+
82
+ # Update indexes
83
+ self._index_manager.insert_row(validated_row, row_id)
84
+
85
+ return row_id
86
+
87
+ def get_row_by_id(self, row_id: int) -> Row | None:
88
+ """Get a row by its ID."""
89
+ try:
90
+ idx = self._row_ids.index(row_id)
91
+ return self._rows[idx].copy()
92
+ except ValueError:
93
+ return None
94
+
95
+ def rows_for_ids(self, row_ids: Iterable[int]) -> list[tuple[int, Row]]:
96
+ """Fetch many rows by id in one pass.
97
+
98
+ get_row_by_id walks every stored id. A primary-key range that
99
+ matches thousands of rows was slower than reading the table once.
100
+ """
101
+ position = {row_id: index for index, row_id in enumerate(self._row_ids)}
102
+ found: list[tuple[int, Row]] = []
103
+ for row_id in row_ids:
104
+ index = position.get(row_id)
105
+ if index is not None:
106
+ found.append((row_id, self._rows[index].copy()))
107
+ return found
108
+
109
+ def get_row_id_by_pk(self, pk_value: Any) -> int | None:
110
+ """Get a row ID by primary key value."""
111
+ pk_column = self.schema.primary_key
112
+ if not pk_column:
113
+ return None
114
+
115
+ index = self._index_manager.get_index(pk_column)
116
+ if not index:
117
+ return None
118
+
119
+ row_ids = index.lookup(pk_value)
120
+ return next(iter(row_ids)) if row_ids else None
121
+
122
+ def get_all_rows(self) -> list[Row]:
123
+ """Get all rows in the table."""
124
+ return [row.copy() for row in self._rows]
125
+
126
+ def get_rows_with_ids(self) -> list[tuple[int, Row]]:
127
+ """Get all rows with their IDs."""
128
+ return [(row_id, row.copy()) for row_id, row in zip(self._row_ids, self._rows, strict=False)]
129
+
130
+ def update_rows(self, row_ids: set[int], updates: dict) -> int:
131
+ """
132
+ Update rows with the given IDs.
133
+
134
+ Args:
135
+ row_ids: Set of row IDs to update
136
+ updates: Dictionary of column updates
137
+
138
+ Returns:
139
+ Number of rows updated
140
+ """
141
+ if not row_ids or not updates:
142
+ return 0
143
+
144
+ # Plan every row before writing. A later duplicate must not keep
145
+ # the first row's new primary key.
146
+ planned: list[tuple[int, int, dict, dict]] = []
147
+ pk_column = self.schema.primary_key
148
+ new_keys: dict[Any, int] = {}
149
+ for i, row_id in enumerate(self._row_ids):
150
+ if row_id not in row_ids:
151
+ continue
152
+ old_row = self._rows[i]
153
+ new_row = old_row.copy()
154
+ for col, val in updates.items():
155
+ col_def = self.schema.get_column(col)
156
+ if col_def is None:
157
+ raise ColumnNotFoundError(col, self.name)
158
+ new_row[col] = col_def.validate(val)
159
+ validated = self.schema.validate_row(new_row, allow_missing=True)
160
+ if pk_column and pk_column in updates:
161
+ new_pk = validated[pk_column]
162
+ if new_pk is not None:
163
+ if new_pk in new_keys:
164
+ raise DuplicateKeyError(new_pk)
165
+ index = self.get_index(pk_column)
166
+ if index and index.has_value(new_pk):
167
+ existing_ids = index.lookup(new_pk)
168
+ if any(existing_id not in row_ids for existing_id in existing_ids):
169
+ raise DuplicateKeyError(new_pk)
170
+ new_keys[new_pk] = row_id
171
+ planned.append((i, row_id, old_row, validated))
172
+
173
+ for i, row_id, old_row, validated in planned:
174
+ self._index_manager.update_row(old_row, validated, row_id)
175
+ self._rows[i] = validated
176
+
177
+ return len(planned)
178
+
179
+ def delete_rows(self, row_ids: set[int]) -> int:
180
+ """
181
+ Delete rows with the given IDs.
182
+
183
+ Args:
184
+ row_ids: Set of row IDs to delete
185
+
186
+ Returns:
187
+ Number of rows deleted
188
+ """
189
+ if not row_ids:
190
+ return 0
191
+
192
+ # Find indices to delete (in reverse order for safe removal)
193
+ indices_to_delete = []
194
+ for i, row_id in enumerate(self._row_ids):
195
+ if row_id in row_ids:
196
+ indices_to_delete.append(i)
197
+
198
+ # Delete in reverse order
199
+ count = 0
200
+ for i in reversed(indices_to_delete):
201
+ row = self._rows[i]
202
+ row_id = self._row_ids[i]
203
+
204
+ # Update indexes
205
+ self._index_manager.delete_row(row, row_id)
206
+
207
+ # Remove row
208
+ self._rows.pop(i)
209
+ self._row_ids.pop(i)
210
+ count += 1
211
+
212
+ return count
213
+
214
+ def scan(self) -> Iterator[tuple[int, Row]]:
215
+ """Iterate over all rows with their IDs."""
216
+ for row_id, row in zip(self._row_ids, self._rows, strict=False):
217
+ yield row_id, row.copy()
218
+
219
+ def get_index(self, column_name: str):
220
+ """Get an index for a column."""
221
+ return self._index_manager.get_index(column_name)
222
+
223
+ def has_index(self, column_name: str) -> bool:
224
+ """Check if an index exists for a column."""
225
+ return self._index_manager.has_index(column_name)
226
+
227
+ def clear(self) -> None:
228
+ """Remove all rows from the table."""
229
+ self._rows.clear()
230
+ self._row_ids.clear()
231
+ self._next_row_id = 0
232
+ self._index_manager.clear()
233
+
234
+ def to_dict(self) -> dict:
235
+ """Serialize table to a dictionary."""
236
+ return {
237
+ 'name': self.name,
238
+ 'schema': self.schema.to_dict(),
239
+ 'rows': self._rows,
240
+ 'row_ids': list(self._row_ids),
241
+ 'next_row_id': self._next_row_id,
242
+ }
243
+
244
+ @classmethod
245
+ def from_dict(cls, data: dict) -> 'Table':
246
+ """Deserialize table from a dictionary."""
247
+ schema = Schema.from_dict(data['schema'])
248
+ table = cls(name=data['name'], schema=schema)
249
+ # Same checks as insert. A hand-edited file must not keep two primary
250
+ # keys or a value the column cannot store.
251
+ table._rows = [schema.validate_row(row, allow_missing=True) for row in data['rows']]
252
+ table._next_row_id = data.get('next_row_id', len(table._rows))
253
+
254
+ # Files written before row_ids existed keep 0..n-1.
255
+ saved_ids = data.get('row_ids')
256
+ if isinstance(saved_ids, list) and len(saved_ids) == len(table._rows):
257
+ table._row_ids = list(saved_ids)
258
+ else:
259
+ table._row_ids = list(range(len(table._rows)))
260
+
261
+ pk = table.schema.primary_key
262
+ if pk:
263
+ index = table._index_manager.get_index(pk)
264
+ if index is None:
265
+ index = table._index_manager.create_index(pk)
266
+ for row, row_id in zip(table._rows, table._row_ids, strict=True):
267
+ value = row.get(pk)
268
+ if value is None:
269
+ continue
270
+ if index.has_value(value):
271
+ raise DuplicateKeyError(value)
272
+ index.insert(value, row_id)
273
+
274
+ return table
minidb_sql/types.py ADDED
@@ -0,0 +1,189 @@
1
+ """Type definitions for MiniDB."""
2
+
3
+ from enum import Enum, auto
4
+ from typing import Any
5
+
6
+
7
+ class ColumnType(Enum):
8
+ """Supported column types in MiniDB."""
9
+
10
+ INTEGER = auto()
11
+ STRING = auto()
12
+ FLOAT = auto()
13
+ BOOLEAN = auto()
14
+
15
+ @classmethod
16
+ def from_string(cls, type_str: str) -> 'ColumnType':
17
+ """Convert a string representation to ColumnType."""
18
+ type_map = {
19
+ 'INTEGER': cls.INTEGER,
20
+ 'INT': cls.INTEGER,
21
+ 'STRING': cls.STRING,
22
+ 'STR': cls.STRING,
23
+ 'TEXT': cls.STRING,
24
+ 'VARCHAR': cls.STRING,
25
+ 'FLOAT': cls.FLOAT,
26
+ 'DOUBLE': cls.FLOAT,
27
+ 'REAL': cls.FLOAT,
28
+ 'BOOLEAN': cls.BOOLEAN,
29
+ 'BOOL': cls.BOOLEAN,
30
+ }
31
+ upper_str = type_str.upper()
32
+ if upper_str not in type_map:
33
+ raise ValueError(f'Unknown column type: {type_str}')
34
+ return type_map[upper_str]
35
+
36
+ def to_string(self) -> str:
37
+ """Convert ColumnType to string representation."""
38
+ return self.name
39
+
40
+ def validate(self, value: Any) -> bool:
41
+ """Check if a value is valid for this column type."""
42
+ if value is None:
43
+ return True # NULL values handled by nullable constraint
44
+
45
+ type_validators = {
46
+ ColumnType.INTEGER: lambda v: isinstance(v, int) and not isinstance(v, bool),
47
+ ColumnType.STRING: lambda v: isinstance(v, str),
48
+ ColumnType.FLOAT: lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
49
+ ColumnType.BOOLEAN: lambda v: isinstance(v, bool),
50
+ }
51
+ return type_validators[self](value)
52
+
53
+ def cast(self, value: Any) -> Any:
54
+ """Attempt to cast a value to this type."""
55
+ if value is None:
56
+ return None
57
+
58
+ try:
59
+ if self == ColumnType.INTEGER:
60
+ if isinstance(value, bool):
61
+ raise TypeError(f'Cannot cast {value!r} to INTEGER')
62
+ if isinstance(value, float):
63
+ if not value.is_integer():
64
+ raise TypeError(f'Cannot cast {value!r} to INTEGER')
65
+ return int(value)
66
+ if isinstance(value, int):
67
+ return value
68
+ return int(value)
69
+ elif self == ColumnType.STRING:
70
+ return str(value)
71
+ elif self == ColumnType.FLOAT:
72
+ if isinstance(value, bool):
73
+ raise TypeError(f'Cannot cast {value!r} to FLOAT')
74
+ return float(value)
75
+ elif self == ColumnType.BOOLEAN:
76
+ if isinstance(value, bool):
77
+ return value
78
+ if isinstance(value, str):
79
+ spelling = {
80
+ 'true': True,
81
+ 'false': False,
82
+ '1': True,
83
+ '0': False,
84
+ 'yes': True,
85
+ 'no': False,
86
+ }
87
+ if value.lower() not in spelling:
88
+ raise TypeError(f'Cannot cast {value!r} to BOOLEAN')
89
+ return spelling[value.lower()]
90
+ raise TypeError(f'Cannot cast {value!r} to BOOLEAN')
91
+ except (ValueError, TypeError) as e:
92
+ raise TypeError(f'Cannot cast {value!r} to {self.name}') from e
93
+
94
+ return value
95
+
96
+
97
+ class TokenType(Enum):
98
+ """Token types for the SQL lexer."""
99
+
100
+ # Keywords
101
+ SELECT = auto()
102
+ FROM = auto()
103
+ WHERE = auto()
104
+ INSERT = auto()
105
+ INTO = auto()
106
+ VALUES = auto()
107
+ UPDATE = auto()
108
+ SET = auto()
109
+ DELETE = auto()
110
+ CREATE = auto()
111
+ TABLE = auto()
112
+ DROP = auto()
113
+ ORDER = auto()
114
+ BY = auto()
115
+ GROUP = auto()
116
+ JOIN = auto()
117
+ INNER = auto()
118
+ LEFT = auto()
119
+ RIGHT = auto()
120
+ ON = auto()
121
+ AND = auto()
122
+ OR = auto()
123
+ NOT = auto()
124
+ IN = auto()
125
+ LIKE = auto()
126
+ ASC = auto()
127
+ DESC = auto()
128
+ PRIMARY = auto()
129
+ KEY = auto()
130
+ NULL = auto()
131
+ NOT_NULL = auto()
132
+ IS = auto()
133
+ LIMIT = auto()
134
+ AS = auto()
135
+ DISTINCT = auto()
136
+ HAVING = auto()
137
+ CASE = auto()
138
+ WHEN = auto()
139
+ THEN = auto()
140
+ ELSE = auto()
141
+ END = auto()
142
+ BEGIN = auto()
143
+ COMMIT = auto()
144
+ ROLLBACK = auto()
145
+
146
+ # Literals
147
+ IDENTIFIER = auto()
148
+ STRING_LITERAL = auto()
149
+ INTEGER_LITERAL = auto()
150
+ FLOAT_LITERAL = auto()
151
+ BOOLEAN_LITERAL = auto()
152
+
153
+ # Operators
154
+ EQUALS = auto()
155
+ NOT_EQUALS = auto()
156
+ GREATER = auto()
157
+ GREATER_EQUAL = auto()
158
+ LESS = auto()
159
+ LESS_EQUAL = auto()
160
+
161
+ # Punctuation
162
+ COMMA = auto()
163
+ DOT = auto()
164
+ LPAREN = auto()
165
+ RPAREN = auto()
166
+ SEMICOLON = auto()
167
+ STAR = auto()
168
+ PLACEHOLDER = auto()
169
+
170
+ # Special
171
+ EOF = auto()
172
+
173
+
174
+ class QueryType(Enum):
175
+ """Types of SQL queries."""
176
+
177
+ SELECT = auto()
178
+ INSERT = auto()
179
+ UPDATE = auto()
180
+ DELETE = auto()
181
+ CREATE_TABLE = auto()
182
+ DROP_TABLE = auto()
183
+
184
+
185
+ # Type alias for row data
186
+ Row = dict[str, Any]
187
+
188
+ # Type alias for query result
189
+ QueryResult = list[Row] | int | None