relations-sqlite3 0.6.8__tar.gz → 0.6.10__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: relations-sqlite3
3
- Version: 0.6.8
3
+ Version: 0.6.10
4
4
  Summary: DB Modeling for SQLite using the sqlite3 library
5
5
  Home-page: https://github.com/relations-dil/python-relations-sqlite3
6
6
  Author: Gaffer Fitch
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE.txt
12
- Requires-Dist: relations-dil>=0.6.13
12
+ Requires-Dist: relations-dil>=0.6.16
13
13
  Requires-Dist: relations-sqlite>=0.6.3
14
14
 
15
15
  # relations-sqlite3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: relations-sqlite3
3
- Version: 0.6.8
3
+ Version: 0.6.10
4
4
  Summary: DB Modeling for SQLite using the sqlite3 library
5
5
  Home-page: https://github.com/relations-dil/python-relations-sqlite3
6
6
  Author: Gaffer Fitch
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE.txt
12
- Requires-Dist: relations-dil>=0.6.13
12
+ Requires-Dist: relations-dil>=0.6.16
13
13
  Requires-Dist: relations-sqlite>=0.6.3
14
14
 
15
15
  # relations-sqlite3
@@ -1,2 +1,2 @@
1
- relations-dil>=0.6.13
1
+ relations-dil>=0.6.16
2
2
  relations-sqlite>=0.6.3
@@ -29,7 +29,9 @@ class Source(relations_sql.SOURCE, relations.Source): # pylint: disable=too-many
29
29
  OP = relations_sqlite.OP
30
30
 
31
31
  AS = relations_sqlite.AS
32
+ OPTIONS = relations_sqlite.OPTIONS
32
33
  FIELDS = relations_sqlite.FIELDS
34
+ COLUMN_NAME = relations_sqlite.COLUMN_NAME
33
35
  TABLE = relations_sqlite.TABLE
34
36
  TABLE_NAME = relations_sqlite.TABLE_NAME
35
37
 
@@ -193,14 +195,30 @@ class Source(relations_sql.SOURCE, relations.Source): # pylint: disable=too-many
193
195
 
194
196
  return model
195
197
 
196
- def retrieve_field(self, field, query):
198
+ def retrieve_record(self, record, query):
197
199
  """
198
- Adds where caluse to query
200
+ Adds where clauses to query for all the fields in the record
201
+ """
202
+
203
+ for field in record._order:
204
+ self.retrieve_field(field, query, record)
205
+
206
+ def retrieve_field(self, field, query, record=None):
207
+ """
208
+ Adds where caluse to query, an injected field is looked up in the field it's stored in
199
209
  """
200
210
 
201
211
  for operator, value in (field.criteria or {}).items():
202
- name = f"{field.store}__{operator}"
203
- extracted = operator.rsplit("__", 1)[0] in (field.extract or {})
212
+
213
+ if field.inject:
214
+ stored, path = field.inject.split("__", 1)
215
+ stored = record._names[stored]
216
+ name = f"{stored.store}__{path}__{operator}"
217
+ extracted = path in (stored.extract or {})
218
+ else:
219
+ name = f"{field.store}__{operator}"
220
+ extracted = operator.rsplit("__", 1)[0] in (field.extract or {})
221
+
204
222
  query.WHERE(self.OP(name, value, EXTRACTED=extracted))
205
223
 
206
224
  def like(self, model, query):
@@ -250,7 +268,17 @@ class Source(relations_sql.SOURCE, relations.Source): # pylint: disable=too-many
250
268
  """
251
269
 
252
270
  for field in (model._sort or model._order or []):
253
- query.ORDER_BY(**{field[1:]: (self.ASC if field[0] == "+" else self.DESC)})
271
+
272
+ name = field[1:]
273
+ field_name, path = (name.split("__", 1) + [None])[:2]
274
+
275
+ # an injected field is sorted by its path in the field it's stored in
276
+
277
+ if model._fields._names[field_name].inject:
278
+ stored, injected = model._fields._names[field_name].inject.split("__", 1)
279
+ name = f"{model._fields._names[stored].store}__{injected}" + (f"__{path}" if path else "")
280
+
281
+ query.ORDER_BY(**{name: (self.ASC if field[0] == "+" else self.DESC)})
254
282
 
255
283
  model._sort = None
256
284
 
@@ -278,6 +306,11 @@ class Source(relations_sql.SOURCE, relations.Source): # pylint: disable=too-many
278
306
  self.retrieve_record(model._record, query)
279
307
  self.like(model, query)
280
308
 
309
+ # a sibling-attribute flat join repeats a model tied to several matching siblings, so
310
+ # count the distinct model id instead of the joined rows
311
+ if getattr(model, "_distinct", False):
312
+ query.FIELDS = self.FIELDS(self.AS("total", self.SQL(f"COUNT(DISTINCT {model.STORE}.{model._id})")))
313
+
281
314
  return query
282
315
 
283
316
  def retrieve_query(self, model):
@@ -287,7 +320,12 @@ class Source(relations_sql.SOURCE, relations.Source): # pylint: disable=too-many
287
320
 
288
321
  query = self.count_query(model)
289
322
 
290
- query.FIELDS = self.FIELDS("*")
323
+ # honor the flat-join DISTINCT marker: dedupe the model rows the join multiplied
324
+ if getattr(model, "_distinct", False):
325
+ query.OPTIONS = self.OPTIONS("DISTINCT")
326
+ query.FIELDS = self.FIELDS(self.COLUMN_NAME("*", table=model.STORE))
327
+ else:
328
+ query.FIELDS = self.FIELDS("*")
291
329
 
292
330
  self.sort(model, query)
293
331
  self.limit(model, query)
@@ -1,19 +1,26 @@
1
1
  #!/usr/bin/env python
2
2
 
3
+ import os
3
4
  from setuptools import setup
4
5
 
5
6
  with open("README.md", "r") as readme_file:
6
7
  long_description = readme_file.read()
7
8
 
9
+ version = os.environ.get("BUILD_VERSION")
10
+
11
+ if version is None:
12
+ with open("VERSION", "r") as version_file:
13
+ version = version_file.read().strip()
14
+
8
15
  setup(
9
16
  name="relations-sqlite3",
10
- version="0.6.8",
17
+ version=version,
11
18
  package_dir = {'': 'lib'},
12
19
  py_modules = [
13
20
  'relations_sqlite3'
14
21
  ],
15
22
  install_requires=[
16
- 'relations-dil>=0.6.13',
23
+ 'relations-dil>=0.6.16',
17
24
  'relations-sqlite>=0.6.3'
18
25
  ],
19
26
  url="https://github.com/relations-dil/python-relations-sqlite3",
@@ -101,6 +101,17 @@ class SisBro(SourceModel):
101
101
 
102
102
  relations.ManyToMany(Sis, Bro, SisBro)
103
103
 
104
+ class Owner(SourceModel):
105
+ id = int
106
+ name = str
107
+
108
+ class Pet(SourceModel):
109
+ id = int
110
+ name = str
111
+ what = dict
112
+
113
+ relations.OneToMany(Owner, Pet, child_inject="what")
114
+
104
115
  def dict_factory(cursor, row):
105
116
  d = {}
106
117
  for idx, col in enumerate(cursor.description):
@@ -218,6 +229,16 @@ CREATE UNIQUE INDEX `test_source`.`simple_name` ON `simple` (`name`);
218
229
 
219
230
  self.source.execute(self.source.define(Simple.thy().define()))
220
231
 
232
+ # an injected key isn't a column, it's stored in the dict field
233
+
234
+ self.source.execute(Owner.define())
235
+ self.source.execute(Pet.define())
236
+
237
+ cursor = self.source.connection.cursor()
238
+ cursor.execute("PRAGMA test_source.table_info(pet)")
239
+ self.assertEqual([column["name"] for column in cursor.fetchall()], ["id", "name", "what"])
240
+ cursor.close()
241
+
221
242
  def test_create_query(self):
222
243
 
223
244
  query = Simple("sure").query()
@@ -323,6 +344,27 @@ CREATE UNIQUE INDEX `test_source`.`simple_name` ON `simple` (`name`);
323
344
 
324
345
  cursor.close()
325
346
 
347
+ # an injected key is stored in the dict field, whether there's a parent or not
348
+
349
+ self.source.execute(Owner.define())
350
+ self.source.execute(Pet.define())
351
+
352
+ owner = Owner("pat")
353
+ owner.pet.add("rex")
354
+ owner.create()
355
+
356
+ stray = Pet("stray").create()
357
+
358
+ cursor = self.source.connection.cursor()
359
+ cursor.execute("SELECT * FROM test_source.pet ORDER BY name")
360
+ rows = [self.source.values_retrieve(stray, row) for row in cursor.fetchall()]
361
+ cursor.close()
362
+
363
+ self.assertEqual(rows, [
364
+ {"id": 1, "name": "rex", "what": {"relations": {"owner": {"id": owner.id}}}},
365
+ {"id": 2, "name": "stray", "what": {}}
366
+ ])
367
+
326
368
  def test_retrieve_field(self):
327
369
 
328
370
  field = relations.Field(int, name="id")
@@ -361,6 +403,37 @@ CREATE UNIQUE INDEX `test_source`.`simple_name` ON `simple` (`name`);
361
403
  self.assertEqual(query.sql, """SELECT WHERE `things__for__0____1`=?""")
362
404
  self.assertEqual(query.args, ['yes'])
363
405
 
406
+ # an injected key is looked up by its path in the field it's stored in
407
+
408
+ record = copy.deepcopy(Pet.thy()._fields)
409
+ field = record._names["owner_id"]
410
+ field.filter(1)
411
+ query = self.source.SELECT()
412
+ self.source.retrieve_field(field, query, record)
413
+ query.generate()
414
+ self.assertEqual(query.sql, """SELECT WHERE json_extract(`what`,?)=?""")
415
+ self.assertEqual(query.args, ['$.relations.owner.id', 1])
416
+
417
+ record = copy.deepcopy(Pet.thy()._fields)
418
+ field = record._names["owner_id"]
419
+ field.filter([1, 2], "in")
420
+ query = self.source.SELECT()
421
+ self.source.retrieve_field(field, query, record)
422
+ query.generate()
423
+ self.assertEqual(query.sql, """SELECT WHERE json_extract(`what`,?) IN (?,?)""")
424
+ self.assertEqual(query.args, ['$.relations.owner.id', 1, 2])
425
+
426
+ def test_retrieve_record(self):
427
+
428
+ record = copy.deepcopy(Pet.thy()._fields)
429
+ record._names["name"].filter("rex")
430
+ record._names["owner_id"].filter(1)
431
+ query = self.source.SELECT()
432
+ self.source.retrieve_record(record, query)
433
+ query.generate()
434
+ self.assertEqual(query.sql, """SELECT WHERE `name`=? AND json_extract(`what`,?)=?""")
435
+ self.assertEqual(query.args, ["rex", "$.relations.owner.id", 1])
436
+
364
437
  def test_like(self):
365
438
 
366
439
  self.source.execute(Unit.define())
@@ -448,6 +521,16 @@ CREATE UNIQUE INDEX `test_source`.`simple_name` ON `simple` (`name`);
448
521
  self.assertEqual(query.args, [])
449
522
  self.assertIsNone(unit._sort)
450
523
 
524
+ # an injected key sorts by its path in the field it's stored in
525
+
526
+ pet = Pet.many()
527
+ pet._sort = ['-owner_id']
528
+ query = self.source.SELECT()
529
+ self.source.sort(pet, query)
530
+ query.generate()
531
+ self.assertEqual(query.sql, """SELECT ORDER BY json_extract(`what`,?) DESC""")
532
+ self.assertEqual(query.args, ['$.relations.owner.id'])
533
+
451
534
  def test_limit(self):
452
535
 
453
536
  unit = Unit.one()
@@ -638,6 +721,19 @@ LIMIT ?""")
638
721
  self.assertEqual(Sis.many(bro_id=[tom.id]).count(), 1)
639
722
  self.assertEqual(Sis.many(bro_id=[999]).count(), 0)
640
723
 
724
+ # an injected key counts like any other
725
+
726
+ self.source.execute(Owner.define())
727
+ self.source.execute(Pet.define())
728
+
729
+ pat = Owner("pat").create()
730
+
731
+ Pet([{"name": "rex", "owner_id": pat.id}, {"name": "stray"}]).create()
732
+
733
+ self.assertEqual(Pet.many().count(), 2)
734
+ self.assertEqual(Pet.many(owner_id=pat.id).count(), 1)
735
+ self.assertEqual(Pet.many(owner_id=99).count(), 0)
736
+
641
737
  def test_values_retrieve(self):
642
738
 
643
739
  model = unittest.mock.MagicMock()
@@ -867,6 +963,37 @@ LIMIT ?""")
867
963
 
868
964
  self.assertEqual(len(Sis.many(bro_id=[999])), 0)
869
965
 
966
+ # an injected key retrieves, filters and relates like a column
967
+
968
+ self.source.execute(Owner.define())
969
+ self.source.execute(Pet.define())
970
+
971
+ pat = Owner("pat").create()
972
+ sam = Owner("sam").create()
973
+
974
+ Pet([
975
+ {"name": "rex", "owner_id": pat.id},
976
+ {"name": "fido", "owner_id": sam.id},
977
+ {"name": "spot", "owner_id": pat.id},
978
+ {"name": "stray"}
979
+ ]).create()
980
+
981
+ self.assertEqual(Pet.many(owner_id=pat.id).name, ["rex", "spot"])
982
+ self.assertEqual(Pet.many(owner_id__in=[pat.id, sam.id]).name, ["fido", "rex", "spot"])
983
+ self.assertEqual(Pet.many(owner_id__null=True).name, ["stray"])
984
+ self.assertEqual(Pet.many(owner_id=99).name, [])
985
+
986
+ self.assertEqual(Pet.one(name="rex").owner.name, "pat")
987
+ self.assertIsNone(Pet.one(name="stray").owner)
988
+
989
+ self.assertEqual(Owner.one(pat.id).pet.name, ["rex", "spot"])
990
+ self.assertEqual(Owner.one(name="pat").pet.name, ["rex", "spot"])
991
+
992
+ self.assertEqual(Pet.many(owner__name="pat").name, ["rex", "spot"])
993
+ self.assertEqual(Owner.many(pet__name="fido").name, ["sam"])
994
+
995
+ self.assertEqual(Pet.many(like="re").name, ["rex"])
996
+
870
997
  def test_retrieve_ties_query(self):
871
998
 
872
999
  self.source.execute(Sis.define())
@@ -917,6 +1044,52 @@ LIMIT ?""")
917
1044
  self.assertEqual(Bro.many(sis_id__all=[jane.id, joan.id]).name, ["Bob"])
918
1045
  self.assertEqual(Bro.many(sis_id__any=[joan.id]).name, ["Bob"])
919
1046
 
1047
+ def test_retrieve_ties_attr(self):
1048
+
1049
+ self.source.execute(Sis.define())
1050
+ self.source.execute(Bro.define())
1051
+ self.source.execute(SisBro.define())
1052
+
1053
+ # M2M attribute filtering: a flat join through the tie table filters on the sibling's
1054
+ # own field. Existence semantics, with the sibling's normal field operators.
1055
+
1056
+ tom = Bro("Tom").create()
1057
+ dick = Bro("Dick").create()
1058
+ harry = Bro("Harry").create()
1059
+
1060
+ Sis("Mary", bro_id=[tom.id, dick.id]).create() # tied to Tom, Dick
1061
+ Sis("Sue", bro_id=[tom.id]).create() # tied to Tom
1062
+ Sis("Ann", bro_id=[dick.id, harry.id]).create() # tied to Dick, Harry
1063
+
1064
+ # tied to a brother with that name
1065
+ self.assertEqual(sorted(Sis.many(bro__name="Tom").name), ["Mary", "Sue"])
1066
+ self.assertEqual(Sis.many(bro__name="Harry").name, ["Ann"])
1067
+ self.assertEqual(len(Sis.many(bro__name="Ghost")), 0)
1068
+
1069
+ # field operators land on the sibling: like
1070
+ self.assertEqual(Sis.many(bro__name__like="arr").name, ["Ann"])
1071
+
1072
+ # DISTINCT: Mary is tied to BOTH Tom and Dick, so an "in" join would match her twice;
1073
+ # she must appear once (retrieve) and count once
1074
+ self.assertEqual(sorted(Sis.many(bro__name__in=["Tom", "Dick"]).name), ["Ann", "Mary", "Sue"])
1075
+ self.assertEqual(Sis.many(bro__name__in=["Tom", "Dick"]).count(), 3)
1076
+
1077
+ # negation is field-level: a tied non-Tom exists (Mary stays via Dick)
1078
+ self.assertEqual(sorted(Sis.many(bro__name__not_in=["Tom"]).name), ["Ann", "Mary"])
1079
+
1080
+ # criteria on the same relation filter the SAME tied brother
1081
+ self.assertEqual(Sis.many(bro__name="Dick", bro__id=harry.id).name, [])
1082
+ self.assertEqual(sorted(Sis.many(bro__name="Dick", bro__id=dick.id).name), ["Ann", "Mary"])
1083
+
1084
+ # symmetric: brothers filtered by a tied sister's name
1085
+ jane = Sis("Jane").create()
1086
+ joan = Sis("Joan").create()
1087
+ Bro("Bab", sis_id=[jane.id, joan.id]).create() # tied to Jane, Joan
1088
+ Bro("Bil", sis_id=[jane.id]).create() # tied to Jane
1089
+
1090
+ self.assertEqual(sorted(Bro.many(sis__name="Jane").name), ["Bab", "Bil"])
1091
+ self.assertEqual(Bro.many(sis__name__in=["Joan"]).name, ["Bab"])
1092
+
920
1093
  def test_titles(self):
921
1094
 
922
1095
  self.source.execute(Unit.define())
@@ -1102,6 +1275,34 @@ LIMIT ?""")
1102
1275
  self.assertEqual(Bro.one(name="Dick").sis.id, [dot.id])
1103
1276
  self.assertEqual(Sis.one(name="Nikki").bro.id, [tom.id])
1104
1277
 
1278
+ # an injected key updates like a column, but not in mass
1279
+
1280
+ self.source.execute(Owner.define())
1281
+ self.source.execute(Pet.define())
1282
+
1283
+ pat = Owner("pat").create()
1284
+ sam = Owner("sam").create()
1285
+
1286
+ Pet([{"name": "rex", "owner_id": pat.id}, {"name": "fido", "owner_id": sam.id}]).create()
1287
+
1288
+ pet = Pet.one(name="rex").retrieve()
1289
+ pet.owner_id = sam.id
1290
+
1291
+ self.assertEqual(pet.update(), 1)
1292
+ self.assertEqual(Pet.one(name="rex").owner_id, sam.id)
1293
+ self.assertEqual(Pet.many(owner_id=sam.id).name, ["fido", "rex"])
1294
+
1295
+ pet.owner_id = None
1296
+
1297
+ self.assertEqual(pet.update(), 1)
1298
+ self.assertIsNone(Pet.one(name="rex").owner_id)
1299
+ self.assertEqual(Pet.many(owner_id__null=True).name, ["rex"])
1300
+
1301
+ self.assertEqual(Pet.one(name="rex").set(owner_id=pat.id).update(), 1)
1302
+ self.assertEqual(Pet.one(name="rex").owner_id, pat.id)
1303
+
1304
+ self.assertRaisesRegex(relations.FieldError, "no mass update with inject", Pet.many(name="rex").set(owner_id=sam.id).update)
1305
+
1105
1306
  def test_delete_query(self):
1106
1307
 
1107
1308
  self.source.execute(Unit.define())
@@ -1182,6 +1383,18 @@ LIMIT ?""")
1182
1383
  self.assertEqual(Sis.one(name="Dot").bro.id, [tom.id])
1183
1384
  self.assertEqual(SisBro.many().count(), 1)
1184
1385
 
1386
+ # an injected key deletes like any other
1387
+
1388
+ self.source.execute(Owner.define())
1389
+ self.source.execute(Pet.define())
1390
+
1391
+ pat = Owner("pat").create()
1392
+
1393
+ Pet([{"name": "rex", "owner_id": pat.id}, {"name": "spot", "owner_id": pat.id}, {"name": "stray"}]).create()
1394
+
1395
+ self.assertEqual(Pet.many(owner_id=pat.id).delete(), 2)
1396
+ self.assertEqual(Pet.many().name, ["stray"])
1397
+
1185
1398
  def test_definition(self):
1186
1399
 
1187
1400
  with open("ddl/general.json", 'w') as ddl_file: