Coverage for snekql/storage.py: 77%

356 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-07 21:13 +0300

1"""SQLite storage declarations and value codecs for table models.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Callable 

6from dataclasses import dataclass 

7from datetime import UTC, datetime 

8from json import JSONDecodeError, dumps, loads 

9from types import EllipsisType 

10from typing import Any, Literal, Self, TypeVar, cast, overload 

11 

12from snekql.errors import ( 

13 FrozenModelError, 

14 ModelDeclarationError, 

15 ModelValidationError, 

16 QueryConstructionError, 

17 SnekqlError, 

18) 

19from snekql.expressions import Assignment, OrderBy, Predicate 

20 

21type SQLiteStorageClass = Literal["INTEGER", "REAL", "TEXT", "BLOB"] 

22 

23 

24@dataclass(frozen=True, kw_only=True) 

25class AttrConfig: 

26 """Constructor bundle for column descriptors. 

27 

28 Keeping descriptor configuration in one value avoids long internal 

29 constructors while preserving explicit storage metadata at each call site. 

30 """ 

31 

32 sqlite_storage_class: SQLiteStorageClass 

33 storage_type_name: str 

34 auto_increment: bool = False 

35 default: object = ... 

36 default_factory: Callable[[], object] | EllipsisType = ... 

37 nullable: bool | None = None 

38 primary_key: bool = False 

39 server_default: object | None = None 

40 unique: bool = False 

41 

42 

43WriteOwnerT = TypeVar("WriteOwnerT") 

44LoadedOwnerT = TypeVar("LoadedOwnerT") 

45OwnerT = TypeVar("OwnerT") 

46WriteT = TypeVar("WriteT") 

47ReadValueT = TypeVar("ReadValueT") 

48 

49 

50class Missing: 

51 """Sentinel type for generated values that are not available yet. 

52 

53 >>> MISSING is Missing() 

54 True 

55 >>> repr(MISSING) 

56 'MISSING' 

57 """ 

58 

59 _instance: Self | None = None 

60 

61 def __new__(cls) -> Self: 

62 if cls._instance is None: 

63 cls._instance = super().__new__(cls) 

64 return cls._instance 

65 

66 def __repr__(self) -> str: 

67 return "MISSING" 

68 

69 

70MISSING = Missing() 

71 

72 

73# Startup schema verification behavior: strict raises on drift, warn logs and 

74# continues. TypeAliasType currently exposes a read-only generic __doc__. 

75type SchemaPolicy = Literal["strict", "warn"] 

76 

77 

78def build_attr(config: AttrConfig) -> Any: 

79 """Build a public column descriptor from normalized storage metadata. 

80 

81 Storage classes differ mostly by SQLite class and logical type name. Keeping 

82 descriptor wiring here gives the storage declaration module one place to 

83 change how declaration metadata becomes query/model behavior. 

84 """ 

85 

86 return Attr[Any, Any, Any, Any, Any](config) 

87 

88 

89class Integer: 

90 """SQLite INTEGER column declaration for table model fields. 

91 

92 >>> class User[S = Pending](Model[S, "User[Fetched]"]): 

93 ... id: User.GenCol[int] = Integer(primary_key=True, default=MISSING) 

94 """ 

95 

96 def __new__( # noqa: PLR0913 

97 cls, 

98 *, 

99 primary_key: bool = False, 

100 auto_increment: bool = False, 

101 nullable: bool | None = None, 

102 unique: bool = False, 

103 default: object = ..., 

104 default_factory: Callable[[], object] | EllipsisType = ..., 

105 ) -> Any: 

106 return build_attr( 

107 AttrConfig( 

108 auto_increment=auto_increment, 

109 default=default, 

110 default_factory=default_factory, 

111 nullable=nullable, 

112 primary_key=primary_key, 

113 unique=unique, 

114 sqlite_storage_class="INTEGER", 

115 storage_type_name="Integer", 

116 ), 

117 ) 

118 

119 

120class Real: 

121 """SQLite REAL column declaration for float-like model values. 

122 

123 >>> class Reading[S = Pending](Model[S, "Reading[Fetched]"]): 

124 ... value: Reading.Col[float] = Real(nullable=False) 

125 """ 

126 

127 def __new__( 

128 cls, 

129 *, 

130 primary_key: bool = False, 

131 nullable: bool | None = None, 

132 unique: bool = False, 

133 default: object = ..., 

134 default_factory: Callable[[], object] | EllipsisType = ..., 

135 ) -> Any: 

136 return build_attr( 

137 AttrConfig( 

138 default=default, 

139 default_factory=default_factory, 

140 nullable=nullable, 

141 primary_key=primary_key, 

142 unique=unique, 

143 sqlite_storage_class="REAL", 

144 storage_type_name="Real", 

145 ), 

146 ) 

147 

148 

149class Text: 

150 """SQLite TEXT column declaration for string model values. 

151 

152 >>> class User[S = Pending](Model[S, "User[Fetched]"]): 

153 ... email: User.Col[str] = Text(nullable=False) 

154 """ 

155 

156 def __new__( 

157 cls, 

158 *, 

159 primary_key: bool = False, 

160 nullable: bool | None = None, 

161 unique: bool = False, 

162 default: object = ..., 

163 default_factory: Callable[[], object] | EllipsisType = ..., 

164 ) -> Any: 

165 return build_attr( 

166 AttrConfig( 

167 default=default, 

168 default_factory=default_factory, 

169 nullable=nullable, 

170 primary_key=primary_key, 

171 unique=unique, 

172 sqlite_storage_class="TEXT", 

173 storage_type_name="Text", 

174 ), 

175 ) 

176 

177 

178class Blob: 

179 """SQLite BLOB column declaration for bytes model values. 

180 

181 >>> class File[S = Pending](Model[S, "File[Fetched]"]): 

182 ... content: File.Col[bytes] = Blob(nullable=False) 

183 """ 

184 

185 def __new__( 

186 cls, 

187 *, 

188 primary_key: bool = False, 

189 nullable: bool | None = None, 

190 unique: bool = False, 

191 default: object = ..., 

192 default_factory: Callable[[], object] | EllipsisType = ..., 

193 ) -> Any: 

194 return build_attr( 

195 AttrConfig( 

196 default=default, 

197 default_factory=default_factory, 

198 nullable=nullable, 

199 primary_key=primary_key, 

200 unique=unique, 

201 sqlite_storage_class="BLOB", 

202 storage_type_name="Blob", 

203 ), 

204 ) 

205 

206 

207class Json: 

208 """SQLite TEXT-backed JSON column declaration. 

209 

210 Values are serialized to JSON text before writes and decoded before fetched 

211 model validation. 

212 

213 >>> class Event[S = Pending](Model[S, "Event[Fetched]"]): 

214 ... payload: Event.Col[dict[str, object]] = Json(nullable=False) 

215 """ 

216 

217 def __new__( 

218 cls, 

219 *, 

220 nullable: bool | None = None, 

221 unique: bool = False, 

222 default: object = ..., 

223 default_factory: Callable[[], object] | EllipsisType = ..., 

224 ) -> Any: 

225 return build_attr( 

226 AttrConfig( 

227 default=default, 

228 default_factory=default_factory, 

229 nullable=nullable, 

230 unique=unique, 

231 sqlite_storage_class="TEXT", 

232 storage_type_name="Json", 

233 ), 

234 ) 

235 

236 

237class Boolean: 

238 """SQLite INTEGER-backed boolean column declaration. 

239 

240 >>> class FeatureFlag[S = Pending](Model[S, "FeatureFlag[Fetched]"]): 

241 ... enabled: FeatureFlag.Col[bool] = Boolean(default=False) 

242 """ 

243 

244 def __new__( 

245 cls, 

246 *, 

247 nullable: bool | None = None, 

248 unique: bool = False, 

249 default: object = ..., 

250 default_factory: Callable[[], object] | EllipsisType = ..., 

251 ) -> Any: 

252 return build_attr( 

253 AttrConfig( 

254 default=default, 

255 default_factory=default_factory, 

256 nullable=nullable, 

257 unique=unique, 

258 sqlite_storage_class="INTEGER", 

259 storage_type_name="Boolean", 

260 ), 

261 ) 

262 

263 

264class DateTime: 

265 """SQLite TEXT-backed timezone-aware datetime column declaration. 

266 

267 >>> class Event[S = Pending](Model[S, "Event[Fetched]"]): 

268 ... created_at: Event.GenCol[datetime] = DateTime( 

269 ... server_default=CurrentTimestamp(), 

270 ... default=MISSING, 

271 ... ) 

272 """ 

273 

274 def __new__( 

275 cls, 

276 *, 

277 server_default: object | None = None, 

278 nullable: bool | None = None, 

279 unique: bool = False, 

280 default: object = ..., 

281 default_factory: Callable[[], object] | EllipsisType = ..., 

282 ) -> Any: 

283 return build_attr( 

284 AttrConfig( 

285 default=default, 

286 default_factory=default_factory, 

287 nullable=nullable, 

288 server_default=server_default, 

289 unique=unique, 

290 sqlite_storage_class="TEXT", 

291 storage_type_name="DateTime", 

292 ), 

293 ) 

294 

295 

296class CurrentTimestamp: 

297 """Server default marker for database-filled UTC timestamps. 

298 

299 >>> DateTime(server_default=CurrentTimestamp(), default=MISSING) 

300 """ 

301 

302 

303class Attr[WriteOwnerT, LoadedOwnerT, OwnerT, WriteT, ReadValueT]: 

304 """Typed model column descriptor used for fields and query construction. 

305 

306 The descriptor exposes pending-state write values on application-created 

307 models, fetched-state read values on runtime materialized models, and query 

308 helper methods on the model class. 

309 """ 

310 

311 def __init__(self, config: AttrConfig) -> None: 

312 self.auto_increment: bool = config.auto_increment 

313 self.default: object = config.default 

314 self.default_factory: Callable[[], object] | EllipsisType = ( 

315 config.default_factory 

316 ) 

317 self.is_generated: bool = False 

318 self.name: str | None = None 

319 self.owner: type[object] | None = None 

320 self.nullable: bool | None = config.nullable 

321 self.primary_key: bool = config.primary_key 

322 self.server_default: object | None = config.server_default 

323 self.sqlite_storage_class: SQLiteStorageClass = config.sqlite_storage_class 

324 self.storage_type_name: str = config.storage_type_name 

325 self.unique: bool = config.unique 

326 

327 def __set_name__(self, owner: type[object], name: str) -> None: 

328 self.name = name 

329 self.owner = owner 

330 

331 @overload 

332 def __get__( 

333 self, instance: None, owner: type[Any] 

334 ) -> Attr[WriteOwnerT, LoadedOwnerT, OwnerT, WriteT, ReadValueT]: ... 

335 @overload 

336 def __get__(self, instance: WriteOwnerT, owner: type[Any]) -> WriteT: ... 

337 @overload 

338 def __get__(self, instance: LoadedOwnerT, owner: type[Any]) -> ReadValueT: ... 

339 def __get__(self, instance: object | None, owner: type[Any]) -> object: 

340 if instance is None: 

341 return self 

342 storage = cast( 

343 "dict[str, object]", 

344 object.__getattribute__(instance, "__dict__"), 

345 ) 

346 return storage[self._require_name()] 

347 

348 def __set__(self, instance: object, value: WriteT) -> None: 

349 if getattr(instance, "_snekql_frozen", False): 

350 msg = "table models are immutable" 

351 raise FrozenModelError(msg) 

352 storage = cast( 

353 "dict[str, object]", 

354 object.__getattribute__(instance, "__dict__"), 

355 ) 

356 storage[self._require_name()] = value 

357 

358 def build_default(self) -> object: 

359 if not isinstance(self.default_factory, EllipsisType): 

360 return self.default_factory() 

361 return self.default 

362 

363 def decode_sqlite(self, value: object) -> object: 

364 """Decode a SQLite value to its logical Python value.""" 

365 

366 return self._decode_backend_value(value, backend="SQLite") 

367 

368 def decode_mariadb(self, value: object) -> object: 

369 """Decode a MariaDB value to its logical Python value.""" 

370 

371 return self._decode_backend_value(value, backend="MariaDB") 

372 

373 def encode_sqlite(self, value: object) -> object: 

374 """Encode a logical Python value for SQLite storage.""" 

375 

376 return self._encode_backend_value(value, backend="SQLite") 

377 

378 def encode_mariadb(self, value: object) -> object: 

379 """Encode a logical Python value for MariaDB storage.""" 

380 

381 return self._encode_backend_value(value, backend="MariaDB") 

382 

383 def _decode_backend_value(self, value: object, *, backend: str) -> object: 

384 """Decode database values using backend-specific storage codecs.""" 

385 

386 try: 

387 if backend == "MariaDB": 

388 decoded_value = self._decode_mariadb(value) 

389 else: 

390 decoded_value = self._decode_sqlite(value) 

391 return self._coerce_logical_value(decoded_value, fetched=True) 

392 except SnekqlError: 

393 raise 

394 except Exception as error: 

395 msg = f"invalid database value for {self._require_name()!r}" 

396 raise ModelValidationError( 

397 msg, 

398 ) from error 

399 

400 def _encode_backend_value(self, value: object, *, backend: str) -> object: 

401 """Encode model values using backend-specific storage codecs.""" 

402 

403 try: 

404 logical_value = self._coerce_logical_value(value, fetched=False) 

405 if logical_value is MISSING: 

406 return MISSING 

407 if backend == "MariaDB": 

408 return self._encode_mariadb(logical_value) 

409 return self._encode_sqlite(logical_value) 

410 except SnekqlError: 

411 raise 

412 except Exception as error: 

413 msg = f"invalid model value for {self._require_name()!r}" 

414 raise ModelValidationError( 

415 msg, 

416 ) from error 

417 

418 def validate_model_value(self, value: object) -> object: 

419 """Validate and normalize a pending model value.""" 

420 

421 try: 

422 return self._coerce_logical_value(value, fetched=False) 

423 except SnekqlError: 

424 raise 

425 except Exception as error: 

426 msg = f"invalid model value for {self._require_name()!r}" 

427 raise ModelValidationError( 

428 msg, 

429 ) from error 

430 

431 def _require_name(self) -> str: 

432 if self.name is None: 

433 msg = "column descriptor is not bound" 

434 raise ModelDeclarationError(msg) 

435 return self.name 

436 

437 def _coerce_logical_value(self, value: object, *, fetched: bool) -> object: 

438 if value is MISSING: 

439 return self._coerce_missing_value(fetched=fetched) 

440 if value is None: 

441 return self._coerce_null_value() 

442 coercers: dict[str, Callable[[object], object]] = { 

443 "Blob": self._coerce_blob_value, 

444 "Boolean": self._coerce_boolean_value, 

445 "DateTime": self._coerce_datetime_value, 

446 "Integer": self._coerce_integer_value, 

447 "Json": self._coerce_json_value, 

448 "Real": self._coerce_real_value, 

449 "Text": self._coerce_text_value, 

450 } 

451 try: 

452 coercer = coercers[self.storage_type_name] 

453 except KeyError as error: 

454 msg = f"unknown storage type {self.storage_type_name!r}" 

455 raise ModelDeclarationError(msg) from error 

456 return coercer(value) 

457 

458 def _coerce_missing_value(self, *, fetched: bool) -> Missing: 

459 if self.is_generated and not fetched: 

460 return MISSING 

461 msg = f"missing generated value for {self._require_name()!r}" 

462 raise ModelValidationError(msg) 

463 

464 def _coerce_null_value(self) -> None: 

465 if self.nullable is False: 

466 msg = f"{self._require_name()!r} cannot be null" 

467 raise ModelValidationError(msg) 

468 

469 def _coerce_blob_value(self, value: object) -> bytes: 

470 if not isinstance(value, bytes): 

471 msg = f"{self._require_name()!r} must be bytes" 

472 raise ModelValidationError(msg) 

473 return value 

474 

475 def _coerce_boolean_value(self, value: object) -> bool: 

476 if type(value) is not bool: 

477 msg = f"{self._require_name()!r} must be a bool" 

478 raise ModelValidationError(msg) 

479 return value 

480 

481 def _coerce_datetime_value(self, value: object) -> datetime: 

482 if not isinstance(value, datetime): 

483 msg = f"{self._require_name()!r} must be a datetime" 

484 raise ModelValidationError(msg) 

485 return self._normalize_datetime(value) 

486 

487 def _coerce_integer_value(self, value: object) -> int: 

488 if type(value) is not int: 

489 msg = f"{self._require_name()!r} must be an int" 

490 raise ModelValidationError(msg) 

491 return value 

492 

493 def _coerce_json_value(self, value: object) -> object: 

494 try: 

495 _ = dumps(value, separators=(",", ":")) 

496 except (TypeError, ValueError) as error: 

497 msg = f"{self._require_name()!r} is not JSON serializable" 

498 raise ModelValidationError(msg) from error 

499 return value 

500 

501 def _coerce_real_value(self, value: object) -> float: 

502 if isinstance(value, bool) or not isinstance(value, int | float): 

503 msg = f"{self._require_name()!r} must be a number" 

504 raise ModelValidationError(msg) 

505 return float(value) 

506 

507 def _coerce_text_value(self, value: object) -> str: 

508 if not isinstance(value, str): 

509 msg = f"{self._require_name()!r} must be a str" 

510 raise ModelValidationError(msg) 

511 return value 

512 

513 def _decode_sqlite(self, value: object) -> object: 

514 if value is None: 

515 return None 

516 if self.storage_type_name == "Json": 

517 if not isinstance(value, str): 

518 msg = f"{self._require_name()!r} database value must be JSON text" 

519 raise ModelValidationError( 

520 msg, 

521 ) 

522 try: 

523 return loads(value) 

524 except JSONDecodeError as error: 

525 msg = f"{self._require_name()!r} database value is not valid JSON" 

526 raise ModelValidationError( 

527 msg, 

528 ) from error 

529 if self.storage_type_name == "Boolean": 

530 if value == 0: 

531 return False 

532 if value == 1: 

533 return True 

534 msg = f"{self._require_name()!r} database value must be 0 or 1" 

535 raise ModelValidationError( 

536 msg, 

537 ) 

538 if self.storage_type_name == "DateTime": 

539 if not isinstance(value, str): 

540 msg = f"{self._require_name()!r} database value must be timestamp text" 

541 raise ModelValidationError( 

542 msg, 

543 ) 

544 return self._decode_datetime_text(value) 

545 return value 

546 

547 def _decode_mariadb(self, value: object) -> object: 

548 if value is None: 

549 return None 

550 decoders: dict[str, Callable[[object], object]] = { 

551 "Boolean": self._decode_mariadb_boolean, 

552 "DateTime": self._decode_mariadb_datetime, 

553 "Json": self._decode_mariadb_json, 

554 } 

555 decoder = decoders.get(self.storage_type_name) 

556 if decoder is None: 

557 return value 

558 return decoder(value) 

559 

560 def _decode_mariadb_json(self, value: object) -> object: 

561 if not isinstance(value, str | bytes | bytearray): 

562 msg = f"{self._require_name()!r} database value must be JSON text" 

563 raise ModelValidationError(msg) 

564 json_text = value.decode() if isinstance(value, bytes | bytearray) else value 

565 try: 

566 return loads(json_text) 

567 except JSONDecodeError as error: 

568 msg = f"{self._require_name()!r} database value is not valid JSON" 

569 raise ModelValidationError(msg) from error 

570 

571 def _decode_mariadb_boolean(self, value: object) -> bool: 

572 if value == 0: 

573 return False 

574 if value == 1: 

575 return True 

576 msg = f"{self._require_name()!r} database value must be 0 or 1" 

577 raise ModelValidationError(msg) 

578 

579 def _decode_mariadb_datetime(self, value: object) -> datetime: 

580 if isinstance(value, datetime): 

581 if value.tzinfo is None: 

582 return value.replace(tzinfo=UTC) 

583 return value 

584 if isinstance(value, str): 

585 return self._decode_mariadb_datetime_text(value) 

586 msg = f"{self._require_name()!r} database value must be a datetime" 

587 raise ModelValidationError(msg) 

588 

589 def _decode_datetime_text(self, value: str) -> datetime: 

590 if not value.endswith("Z"): 

591 msg = f"{self._require_name()!r} timestamp must end with Z" 

592 raise ModelValidationError( 

593 msg, 

594 ) 

595 try: 

596 parsed = datetime.fromisoformat(f"{value[:-1]}+00:00") 

597 except ValueError as error: 

598 msg = f"{self._require_name()!r} timestamp is not valid ISO text" 

599 raise ModelValidationError( 

600 msg, 

601 ) from error 

602 return self._normalize_datetime(parsed) 

603 

604 def _decode_mariadb_datetime_text(self, value: str) -> datetime: 

605 try: 

606 parsed = datetime.fromisoformat(value.replace(" ", "T")) 

607 except ValueError as error: 

608 msg = f"{self._require_name()!r} timestamp is not valid MariaDB text" 

609 raise ModelValidationError(msg) from error 

610 if parsed.tzinfo is None: 

611 parsed = parsed.replace(tzinfo=UTC) 

612 return self._normalize_datetime(parsed) 

613 

614 def _encode_sqlite(self, value: object) -> object: 

615 if value is None: 

616 return None 

617 if self.storage_type_name == "Json": 

618 try: 

619 return dumps(value, separators=(",", ":")) 

620 except (TypeError, ValueError) as error: 

621 msg = f"{self._require_name()!r} is not JSON serializable" 

622 raise ModelValidationError( 

623 msg, 

624 ) from error 

625 if self.storage_type_name == "Boolean": 

626 return 1 if value else 0 

627 if self.storage_type_name == "DateTime": 

628 timestamp = cast("datetime", value) 

629 return ( 

630 timestamp.strftime("%Y-%m-%dT%H:%M:%S.") 

631 + f"{timestamp.microsecond // 1000:03d}Z" 

632 ) 

633 return value 

634 

635 def _encode_mariadb(self, value: object) -> object: 

636 if value is None: 

637 return None 

638 if self.storage_type_name == "Json": 

639 try: 

640 return dumps(value, separators=(",", ":")) 

641 except (TypeError, ValueError) as error: 

642 msg = f"{self._require_name()!r} is not JSON serializable" 

643 raise ModelValidationError(msg) from error 

644 if self.storage_type_name == "Boolean": 

645 return 1 if value else 0 

646 if self.storage_type_name == "DateTime": 

647 timestamp = cast("datetime", value) 

648 return ( 

649 timestamp.strftime("%Y-%m-%d %H:%M:%S.") 

650 + f"{timestamp.microsecond // 1000:03d}" 

651 ) 

652 return value 

653 

654 def _normalize_datetime(self, value: datetime) -> datetime: 

655 if value.tzinfo is None or value.utcoffset() is None: 

656 msg = f"{self._require_name()!r} must be timezone-aware" 

657 raise ModelValidationError( 

658 msg, 

659 ) 

660 utc_value = value.astimezone(UTC) 

661 milliseconds = utc_value.microsecond // 1000 

662 return utc_value.replace(microsecond=milliseconds * 1000) 

663 

664 def eq(self, value: ReadValueT) -> Predicate[OwnerT]: 

665 if value is None: 

666 msg = "eq(None) is invalid; use is_null()" 

667 raise QueryConstructionError(msg) 

668 return Predicate(kind="eq", column=self, value=value) 

669 

670 def ne(self, value: ReadValueT) -> Predicate[OwnerT]: 

671 if value is None: 

672 msg = "ne(None) is invalid; use is_not_null()" 

673 raise QueryConstructionError(msg) 

674 return Predicate(kind="ne", column=self, value=value) 

675 

676 def is_null(self) -> Predicate[OwnerT]: 

677 return Predicate(kind="is_null", column=self) 

678 

679 def is_not_null(self) -> Predicate[OwnerT]: 

680 return Predicate(kind="is_not_null", column=self) 

681 

682 def in_(self, *values: ReadValueT) -> Predicate[OwnerT]: 

683 if not values: 

684 msg = "in_() requires at least one value" 

685 raise QueryConstructionError(msg) 

686 if any(candidate is None for candidate in values): 

687 msg = "in_() values cannot be None" 

688 raise QueryConstructionError(msg) 

689 return Predicate(kind="in", column=self, values=values) 

690 

691 def not_in(self, *values: ReadValueT) -> Predicate[OwnerT]: 

692 if not values: 

693 msg = "not_in() requires at least one value" 

694 raise QueryConstructionError(msg) 

695 if any(candidate is None for candidate in values): 

696 msg = "not_in() values cannot be None" 

697 raise QueryConstructionError(msg) 

698 return Predicate(kind="not_in", column=self, values=values) 

699 

700 def like(self, pattern: str) -> Predicate[OwnerT]: 

701 if self.storage_type_name != "Text": 

702 msg = "like() is only valid for text columns" 

703 raise QueryConstructionError(msg) 

704 return Predicate(kind="like", column=self, value=pattern) 

705 

706 def not_like(self, pattern: str) -> Predicate[OwnerT]: 

707 if self.storage_type_name != "Text": 

708 msg = "not_like() is only valid for text columns" 

709 raise QueryConstructionError(msg) 

710 return Predicate(kind="not_like", column=self, value=pattern) 

711 

712 def asc(self) -> OrderBy[OwnerT]: 

713 return OrderBy(column=self, direction="ASC") 

714 

715 def desc(self) -> OrderBy[OwnerT]: 

716 return OrderBy(column=self, direction="DESC") 

717 

718 def to(self, value: ReadValueT) -> Assignment[OwnerT]: 

719 return Assignment(column=self, value=value)