orfdata 0.7.0__tar.gz → 0.8.0__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.
Files changed (108) hide show
  1. {orfdata-0.7.0 → orfdata-0.8.0}/MANIFEST.in +1 -0
  2. {orfdata-0.7.0/orfdata.egg-info → orfdata-0.8.0}/PKG-INFO +1 -1
  3. {orfdata-0.7.0 → orfdata-0.8.0}/docs/measurements.md +2 -1
  4. orfdata-0.8.0/docs/storage.md +45 -0
  5. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/dataset.py +121 -54
  6. orfdata-0.8.0/orfdata/version.py +1 -0
  7. {orfdata-0.7.0 → orfdata-0.8.0/orfdata.egg-info}/PKG-INFO +1 -1
  8. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata.egg-info/SOURCES.txt +11 -1
  9. orfdata-0.8.0/tests/data/storage/README.md +15 -0
  10. orfdata-0.8.0/tests/data/storage/generate.py +74 -0
  11. orfdata-0.8.0/tests/data/storage/v1.json +582 -0
  12. orfdata-0.8.0/tests/data/storage/v1.sqlite +0 -0
  13. orfdata-0.8.0/tests/data/storage/v2.json +584 -0
  14. orfdata-0.8.0/tests/data/storage/v2.sqlite +0 -0
  15. orfdata-0.8.0/tests/data/storage/v3.json +613 -0
  16. orfdata-0.8.0/tests/data/storage/v3.sqlite +0 -0
  17. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_pairing.py +4 -8
  18. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_reference_mapping.py +2 -6
  19. orfdata-0.8.0/tests/test_storage.py +76 -0
  20. orfdata-0.7.0/orfdata/version.py +0 -1
  21. {orfdata-0.7.0 → orfdata-0.8.0}/LICENSE +0 -0
  22. {orfdata-0.7.0 → orfdata-0.8.0}/README.md +0 -0
  23. {orfdata-0.7.0 → orfdata-0.8.0}/deploy.sh +0 -0
  24. {orfdata-0.7.0 → orfdata-0.8.0}/docs/copy-yeast.md +0 -0
  25. {orfdata-0.7.0 → orfdata-0.8.0}/docs/design.md +0 -0
  26. {orfdata-0.7.0 → orfdata-0.8.0}/docs/pairing.md +0 -0
  27. {orfdata-0.7.0 → orfdata-0.8.0}/docs/pfizer-protein.md +0 -0
  28. {orfdata-0.7.0 → orfdata-0.8.0}/docs/reference.md +0 -0
  29. {orfdata-0.7.0 → orfdata-0.8.0}/docs/source-references.md +0 -0
  30. {orfdata-0.7.0 → orfdata-0.8.0}/docs/species.md +0 -0
  31. {orfdata-0.7.0 → orfdata-0.8.0}/lint.sh +0 -0
  32. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/__init__.py +0 -0
  33. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/build.py +0 -0
  34. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/catalog.py +0 -0
  35. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/cli.py +0 -0
  36. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/datasets.json +0 -0
  37. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/orf-evidence/AUTHOR-LICENSE.txt +0 -0
  38. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/orf-evidence/NOTICE.txt +0 -0
  39. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/orf-evidence/catalog.json.gz +0 -0
  40. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/orf-evidence/manifest.json +0 -0
  41. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/uorf-evidence/AUTHOR-LICENSE.txt +0 -0
  42. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/uorf-evidence/NOTICE.txt +0 -0
  43. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/uorf-evidence/catalog.json.gz +0 -0
  44. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/data/uorf-evidence/manifest.json +0 -0
  45. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/dataset_cli.py +0 -0
  46. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/datasets.py +0 -0
  47. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/initiation_context.py +0 -0
  48. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/measurements.py +0 -0
  49. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/model.py +0 -0
  50. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/pairing.py +0 -0
  51. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/reference.py +0 -0
  52. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/reference_mapping.py +0 -0
  53. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/sequence.py +0 -0
  54. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/sources/__init__.py +0 -0
  55. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/sources/copy.py +0 -0
  56. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata/sources/pfizer.py +0 -0
  57. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata.egg-info/dependency_links.txt +0 -0
  58. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata.egg-info/entry_points.txt +0 -0
  59. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata.egg-info/requires.txt +0 -0
  60. {orfdata-0.7.0 → orfdata-0.8.0}/orfdata.egg-info/top_level.txt +0 -0
  61. {orfdata-0.7.0 → orfdata-0.8.0}/pyproject.toml +0 -0
  62. {orfdata-0.7.0 → orfdata-0.8.0}/setup.cfg +0 -0
  63. {orfdata-0.7.0 → orfdata-0.8.0}/test.sh +0 -0
  64. {orfdata-0.7.0 → orfdata-0.8.0}/tests/conftest.py +0 -0
  65. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905474.soft +0 -0
  66. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905474_KL9_fpkm.txt.gz +0 -0
  67. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905476.soft +0 -0
  68. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905476_KL10_fpkm.txt.gz +0 -0
  69. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905478.soft +0 -0
  70. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905478_KL11_fpkm.txt.gz +0 -0
  71. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905479.soft +0 -0
  72. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/GSM1905479_KL12_fpkm.txt.gz +0 -0
  73. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/NOTICE.md +0 -0
  74. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/expected.json +0 -0
  75. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/geo-series.soft +0 -0
  76. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/manifest.json +0 -0
  77. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/methods.pdf +0 -0
  78. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/paper.xml +0 -0
  79. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/pmc-metadata.json +0 -0
  80. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/qconcat.fasta +0 -0
  81. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/copy/workbook.xlsx +0 -0
  82. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/NOTICE.md +0 -0
  83. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/dog/expected.json +0 -0
  84. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/dog/manifest.json +0 -0
  85. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/dog/protein.tsv +0 -0
  86. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/dog/search.fasta +0 -0
  87. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/human/expected.json +0 -0
  88. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/human/manifest.json +0 -0
  89. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/human/protein.tsv +0 -0
  90. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/human/search.fasta +0 -0
  91. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/mouse/expected.json +0 -0
  92. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/mouse/manifest.json +0 -0
  93. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/mouse/protein.tsv +0 -0
  94. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/pfizer/mouse/search.fasta +0 -0
  95. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/references/README.md +0 -0
  96. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/references/canis_lupus_familiaris.json +0 -0
  97. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/references/context-keys-v0.4.json +0 -0
  98. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/references/homo_sapiens.json +0 -0
  99. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/references/mus_musculus.json +0 -0
  100. {orfdata-0.7.0 → orfdata-0.8.0}/tests/data/references/saccharomyces_cerevisiae.json +0 -0
  101. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_copy.py +0 -0
  102. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_cross_reference_pairing.py +0 -0
  103. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_initiation_context.py +0 -0
  104. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_measurements.py +0 -0
  105. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_pfizer.py +0 -0
  106. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_reference_context.py +0 -0
  107. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_reference_orf.py +0 -0
  108. {orfdata-0.7.0 → orfdata-0.8.0}/tests/test_uorf_catalog.py +0 -0
@@ -2,3 +2,4 @@ include lint.sh test.sh deploy.sh
2
2
  recursive-include docs *.md
3
3
  recursive-include tests *.py *.json *.md *.tsv *.fasta
4
4
  recursive-include tests/data/copy *.xlsx *.gz *.soft *.xml *.pdf
5
+ recursive-include tests/data/storage *.sqlite
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orfdata
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Reference open reading frames and experimental translation evidence on annotated transcripts
5
5
  Author: OpenVax
6
6
  License-Expression: Apache-2.0
@@ -145,4 +145,5 @@ arbitrary other taxa use the same schema. The core API needs neither pandas nor
145
145
  PyArrow. Optional `orfdata[columnar]` enables `dataset.export_parquet(path)`;
146
146
  query columns accompany full measurement JSON, preserving exact values and
147
147
  provenance even when a floating-point analysis column cannot represent an
148
- integer exactly.
148
+ integer exactly. [Compact storage and compatibility](storage.md) describes
149
+ new SQLite v4 writes, legacy readers and actual-source size/query benchmarks.
@@ -0,0 +1,45 @@
1
+ # Quantitative storage
2
+
3
+ ORFdata 0.8 writes compact SQLite storage version 4. `Measurement`,
4
+ `QuantitativeDataset`, the registry and CLI keep the same public records and
5
+ query semantics. Human, mouse, dog, yeast and arbitrary taxa retain separate
6
+ reference-scoped identities.
7
+
8
+ References, targets and assays are stored once and addressed by internal integer
9
+ keys. Measurements retain their values and metadata; repeated required fields
10
+ are reconstructed from indexed columns and the dataset manifest. Fields equal
11
+ to the fixed version-4 defaults are omitted from stored JSON and restored on read.
12
+ Zeros, nulls, censoring, uncertainty, nested provenance and derived inputs retain
13
+ their meaning. Foreign keys prevent cross-reference target/assay assignments.
14
+ Internal integer keys never appear as public biological identifiers.
15
+
16
+ Published storage versions 1, 2 and 3 remain readable. There is no automatic
17
+ migration or replacement of downloaded artifacts. A new source rebuild can use
18
+ a different storage layout and therefore a different file checksum while
19
+ returning the same records. Registry downloads always verify the exact published
20
+ artifact's hash; source builds retain their own DataCache receipts.
21
+
22
+ The implementation streams observations and bounds its target cache to 131,072
23
+ entries. SQLite index statistics are populated during creation so a narrow
24
+ target query can use the target index instead of scanning an entire reference.
25
+ Raw SQL table layouts are internal; use the Python/CLI query and export APIs.
26
+
27
+ ## Actual-source benchmark
28
+
29
+ The first 100,000 rows of the native Pfizer/GEO RNA table contain four dog
30
+ libraries and 30,941 genes, including measured zeros. Source gzip SHA-256:
31
+ `a22780f92ffd0a028e30fe3392d73d01f787eb5a62b2043e8d26940191cbac70`.
32
+ This is a storage benchmark slice, not a newly released experimental dataset.
33
+
34
+ | Measurement | Storage v3 | Storage v4 |
35
+ |---|---:|---:|
36
+ | Installed size | 121.6 MB | 38.6 MB |
37
+ | Gzip size | 13.4 MB | 7.5 MB |
38
+ | Build time | 8.90 s | 4.28 s |
39
+ | One-gene query (four results) | 135 ms | 1.5 ms |
40
+ | Peak build RSS | 39.8 MB | 52.1 MB |
41
+
42
+ All 100,000 reconstructed observations were compared field for field. Timings
43
+ are local measurements on Python 3.12, not cross-machine guarantees; the bounded
44
+ target cache trades some memory for repeated-target lookup speed. Legacy tests
45
+ also use actual databases written by ORFdata 0.3.0, 0.4.0 and 0.5.0.
@@ -5,6 +5,8 @@ import json
5
5
  import os
6
6
  import sqlite3
7
7
  from contextlib import closing
8
+ from dataclasses import MISSING, fields
9
+ from functools import lru_cache
8
10
  from pathlib import Path
9
11
 
10
12
  from .measurements import (
@@ -16,6 +18,7 @@ from .measurements import (
16
18
  MeasurementReference,
17
19
  Sample,
18
20
  TargetMapping,
21
+ _plain,
19
22
  )
20
23
  from .pairing import (
21
24
  PAIRING_BASES,
@@ -30,38 +33,50 @@ from .reference_mapping import MappedMeasurement, ReferenceMapping
30
33
 
31
34
  _SCHEMA = """
32
35
  CREATE TABLE metadata (name TEXT PRIMARY KEY, value TEXT NOT NULL);
33
- CREATE TABLE reference (key TEXT PRIMARY KEY, record TEXT NOT NULL);
36
+ CREATE TABLE reference (i INTEGER PRIMARY KEY, key TEXT UNIQUE NOT NULL,
37
+ record TEXT NOT NULL, UNIQUE(key, i));
34
38
  CREATE TABLE target (
35
- reference_key TEXT NOT NULL REFERENCES reference(key),
39
+ i INTEGER PRIMARY KEY, reference_i INTEGER NOT NULL,
40
+ reference_key TEXT NOT NULL,
36
41
  key TEXT NOT NULL, kind TEXT NOT NULL, namespace TEXT NOT NULL,
37
- identifier TEXT NOT NULL, PRIMARY KEY(reference_key, key));
42
+ identifier TEXT NOT NULL, UNIQUE(reference_key, key), UNIQUE(reference_i, i),
43
+ FOREIGN KEY(reference_key, reference_i) REFERENCES reference(key, i));
38
44
  CREATE TABLE sample (
39
45
  reference_key TEXT NOT NULL REFERENCES reference(key),
40
46
  id TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id));
41
47
  CREATE TABLE assay (
48
+ i INTEGER PRIMARY KEY, reference_i INTEGER NOT NULL,
42
49
  reference_key TEXT NOT NULL, id TEXT NOT NULL, sample_id TEXT NOT NULL,
43
- record TEXT NOT NULL, PRIMARY KEY(reference_key, id),
50
+ record TEXT NOT NULL, UNIQUE(reference_key, id), UNIQUE(reference_i, i),
44
51
  UNIQUE(reference_key, id, sample_id),
52
+ FOREIGN KEY(reference_key, reference_i) REFERENCES reference(key, i),
45
53
  FOREIGN KEY(reference_key, sample_id) REFERENCES sample(reference_key, id));
46
54
  CREATE TABLE mapping (
47
55
  reference_key TEXT NOT NULL REFERENCES reference(key),
48
56
  id TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id));
49
- CREATE TABLE measurement (
50
- reference_key TEXT NOT NULL, id TEXT NOT NULL, target_key TEXT NOT NULL,
51
- sample_id TEXT NOT NULL, assay_id TEXT NOT NULL, quantity TEXT NOT NULL,
52
- status TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id),
53
- FOREIGN KEY(reference_key, target_key) REFERENCES target(reference_key, key),
54
- FOREIGN KEY(reference_key, assay_id, sample_id)
55
- REFERENCES assay(reference_key, id, sample_id));
56
- CREATE TABLE input_measurement (
57
- reference_key TEXT NOT NULL, output_id TEXT NOT NULL,
58
- input_reference_key TEXT NOT NULL, input_id TEXT NOT NULL,
59
- PRIMARY KEY(reference_key, output_id, input_reference_key, input_id),
60
- FOREIGN KEY(reference_key, output_id) REFERENCES measurement(reference_key, id),
61
- FOREIGN KEY(input_reference_key, input_id) REFERENCES measurement(reference_key, id));
57
+ CREATE TABLE observation (
58
+ i INTEGER PRIMARY KEY, reference_i INTEGER NOT NULL,
59
+ id TEXT NOT NULL, target_i INTEGER NOT NULL, assay_i INTEGER NOT NULL,
60
+ quantity TEXT NOT NULL, status TEXT NOT NULL, record TEXT NOT NULL,
61
+ UNIQUE(reference_i, id),
62
+ FOREIGN KEY(reference_i, target_i) REFERENCES target(reference_i, i),
63
+ FOREIGN KEY(reference_i, assay_i) REFERENCES assay(reference_i, i));
64
+ CREATE TABLE observation_input (
65
+ output_i INTEGER NOT NULL REFERENCES observation(i),
66
+ input_i INTEGER NOT NULL REFERENCES observation(i), PRIMARY KEY(output_i, input_i));
67
+ CREATE VIEW measurement AS
68
+ SELECT r.key reference_key, m.id, t.key target_key, a.sample_id,
69
+ a.id assay_id, m.quantity, m.status, m.record
70
+ FROM observation m JOIN reference r ON r.i=m.reference_i
71
+ JOIN target t ON t.i=m.target_i JOIN assay a ON a.i=m.assay_i;
72
+ CREATE VIEW input_measurement AS
73
+ SELECT r.key reference_key, o.id output_id, s.key input_reference_key, i.id input_id
74
+ FROM observation_input p JOIN observation o ON o.i=p.output_i
75
+ JOIN observation i ON i.i=p.input_i JOIN reference r ON r.i=o.reference_i
76
+ JOIN reference s ON s.i=i.reference_i;
62
77
  CREATE INDEX target_lookup ON target(reference_key, namespace, kind, identifier);
63
- CREATE INDEX observation_lookup ON measurement(reference_key, target_key, sample_id, assay_id);
64
- CREATE INDEX assay_lookup ON measurement(reference_key, assay_id, quantity, status);
78
+ CREATE INDEX observation_lookup ON observation(reference_i, target_i, assay_i);
79
+ CREATE INDEX assay_lookup ON observation(reference_i, assay_i, quantity, status);
65
80
  CREATE TABLE assay_pair (
66
81
  reference_key TEXT NOT NULL REFERENCES reference(key), id TEXT NOT NULL,
67
82
  right_reference_key TEXT NOT NULL REFERENCES reference(key),
@@ -100,6 +115,29 @@ def _target_key(target):
100
115
  return hashlib.sha256(_json(target.key).encode()).hexdigest()
101
116
 
102
117
 
118
+ _STORED_FIELDS = tuple(f for f in fields(Measurement) if f.name not in {
119
+ "measurement_id", "target", "sample_id", "assay_id", "quantity", "status",
120
+ "dataset_id", "dataset_version"})
121
+ _V4_DEFAULTS = {
122
+ "origin": "observed", "scale": "linear", "log_base": None,
123
+ "uncertainty": None, "uncertainty_type": None, "detection_limit": None,
124
+ "input_measurements": (), "input_references": (), "derivation": {}, "provenance": {},
125
+ }
126
+
127
+
128
+ def _measurement_record(observation):
129
+ # Schema defaults are fixed here, independent of future model defaults.
130
+ # Do not serialize the target
131
+ # and then discard it: its full reference is already stored once.
132
+ result = {}
133
+ for field in _STORED_FIELDS:
134
+ value = getattr(observation, field.name)
135
+ default = _V4_DEFAULTS.get(field.name, MISSING)
136
+ if default is MISSING or value != default:
137
+ result[field.name] = _plain(value)
138
+ return result
139
+
140
+
103
141
  class QuantitativeDataset:
104
142
  """A read-only release. Constructing or querying it never downloads data.
105
143
 
@@ -112,7 +150,7 @@ class QuantitativeDataset:
112
150
  self.path = Path(path).resolve()
113
151
  with closing(self._connect()) as connection:
114
152
  self._storage_version = connection.execute("PRAGMA user_version").fetchone()[0]
115
- if self._storage_version not in (1, 2, 3):
153
+ if self._storage_version not in (1, 2, 3, 4):
116
154
  raise ValueError("Unsupported quantitative database schema")
117
155
  row = connection.execute("SELECT value FROM metadata WHERE name='manifest'").fetchone()
118
156
  if row is None:
@@ -146,25 +184,46 @@ class QuantitativeDataset:
146
184
  connection = sqlite3.connect(path)
147
185
  connection.execute("PRAGMA foreign_keys=ON")
148
186
  connection.executescript(_SCHEMA)
149
- connection.execute("PRAGMA user_version=3")
187
+ connection.execute("PRAGMA user_version=4")
150
188
  connection.execute("INSERT INTO metadata VALUES ('manifest', ?)",
151
189
  (_json(manifest.to_dict()),))
190
+ reference_ids = {}
152
191
  for reference in manifest.references:
153
- connection.execute("INSERT INTO reference VALUES (?, ?)",
154
- (reference.key, _json(reference.to_dict())))
192
+ cursor = connection.execute("INSERT INTO reference (key, record) VALUES (?, ?)",
193
+ (reference.key, _json(reference.to_dict())))
194
+ reference_ids[reference.key] = cursor.lastrowid
195
+
196
+ @lru_cache(maxsize=131072)
197
+ def insert_target(target):
198
+ reference_key = target.reference.key
199
+ if reference_key not in reference_ids:
200
+ raise sqlite3.IntegrityError("Target reference absent from dataset")
201
+ reference_i = reference_ids[reference_key]
202
+ key = _target_key(target)
203
+ connection.execute("INSERT OR IGNORE INTO target "
204
+ "(reference_i, reference_key, key, kind, namespace, identifier) "
205
+ "VALUES (?, ?, ?, ?, ?, ?)",
206
+ (reference_i, reference_key, key, target.kind, target.namespace, target.identifier))
207
+ target_i = connection.execute("SELECT i FROM target WHERE reference_key=? AND key=?",
208
+ (reference_key, key)).fetchone()[0]
209
+ return reference_i, target_i, key
155
210
  for sample in samples:
156
211
  if not isinstance(sample, Sample):
157
212
  raise TypeError("samples must contain Samples")
158
213
  connection.execute("INSERT INTO sample VALUES (?, ?, ?)",
159
214
  (sample.reference.key, sample.sample_id, _json(sample.to_dict())))
215
+ assay_ids = {}
160
216
  for assay in assays:
161
217
  if not isinstance(assay, Assay):
162
218
  raise TypeError("assays must contain Assays")
163
219
  if assay.kind not in manifest.assay_kinds:
164
220
  raise ValueError("Assay kind absent from manifest coverage")
165
- connection.execute("INSERT INTO assay VALUES (?, ?, ?, ?)",
166
- (assay.reference.key, assay.assay_id, assay.sample_id,
167
- _json(assay.to_dict())))
221
+ reference_i = reference_ids.get(assay.reference.key)
222
+ cursor = connection.execute("INSERT INTO assay "
223
+ "(reference_i, reference_key, id, sample_id, record) VALUES (?, ?, ?, ?, ?)",
224
+ (reference_i, assay.reference.key, assay.assay_id, assay.sample_id,
225
+ _json(assay.to_dict())))
226
+ assay_ids[(reference_i, assay.assay_id, assay.sample_id)] = cursor.lastrowid
168
227
  for pair in pairings:
169
228
  if not isinstance(pair, AssayPair):
170
229
  raise TypeError("pairings must contain AssayPairs")
@@ -194,9 +253,7 @@ class QuantitativeDataset:
194
253
  if (relation.dataset_id, relation.dataset_version) != (manifest.dataset_id, manifest.version):
195
254
  raise ValueError("Target relation belongs to a different dataset release")
196
255
  for target in (relation.left, relation.right):
197
- connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
198
- (target.reference.key, _target_key(target), target.kind,
199
- target.namespace, target.identifier))
256
+ insert_target(target)
200
257
  for index, (first, second) in enumerate(((relation.left, relation.right), (relation.right, relation.left))):
201
258
  _check_one_to_one(relation_links[index],
202
259
  (relation.relation, first.key, second.reference.key, second.kind, second.namespace), second.key,
@@ -218,9 +275,7 @@ class QuantitativeDataset:
218
275
  if (mapping.dataset_id, mapping.dataset_version) != (manifest.dataset_id, manifest.version):
219
276
  raise ValueError("Reference mapping belongs to a different dataset release")
220
277
  for target in (mapping.source, *mapping.targets):
221
- connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
222
- (target.reference.key, _target_key(target), target.kind,
223
- target.namespace, target.identifier))
278
+ insert_target(target)
224
279
  connection.execute("INSERT INTO reference_mapping VALUES (?, ?, ?, ?, ?)",
225
280
  (mapping.source.reference.key, mapping.mapping_id, _target_key(mapping.source),
226
281
  mapping.target_reference.key, _json(mapping.to_dict())))
@@ -238,33 +293,39 @@ class QuantitativeDataset:
238
293
  raise ValueError("Measurement provenance names an unknown source asset")
239
294
  if source is None and not (observation.input_measurements or observation.input_references):
240
295
  raise ValueError("Measurement requires a source_asset or input measurements")
241
- reference_key = observation.target.reference.key
242
- inputs = [(reference_key, identifier) for identifier in observation.input_measurements]
243
- inputs.extend((value.reference.key, value.measurement_id) for value in observation.input_references)
296
+ reference_i, target_i, _ = insert_target(observation.target)
297
+ inputs = [(reference_i, identifier) for identifier in observation.input_measurements]
298
+ inputs.extend((reference_ids.get(value.reference.key), value.measurement_id)
299
+ for value in observation.input_references)
300
+ input_ids = []
244
301
  for input_reference, identifier in inputs:
245
- if connection.execute(
246
- "SELECT 1 FROM measurement WHERE reference_key=? AND id=?",
302
+ found = connection.execute(
303
+ "SELECT i FROM observation WHERE reference_i=? AND id=?",
247
304
  (input_reference, identifier),
248
- ).fetchone() is None:
305
+ ).fetchone()
306
+ if found is None:
249
307
  raise ValueError("Derived inputs must precede outputs in their declared reference contexts")
250
- target = observation.target
251
- target_key = _target_key(target)
252
- connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
253
- (reference_key, target_key, target.kind,
254
- target.namespace, target.identifier))
255
- record = observation.to_dict()
256
- record.pop("target")
257
- connection.execute("INSERT INTO measurement VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
258
- (reference_key, observation.measurement_id, target_key,
259
- observation.sample_id, observation.assay_id,
260
- observation.quantity, observation.status, _json(record)))
261
- connection.executemany("INSERT INTO input_measurement VALUES (?, ?, ?, ?)",
262
- ((reference_key, observation.measurement_id, input_reference, identifier)
263
- for input_reference, identifier in inputs))
264
- counts = {table: connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
308
+ input_ids.append(found[0])
309
+ assay_i = assay_ids.get((reference_i, observation.assay_id, observation.sample_id))
310
+ if assay_i is None:
311
+ raise sqlite3.IntegrityError("Observation assay/sample absent from its reference context")
312
+ cursor = connection.execute("INSERT INTO observation "
313
+ "(reference_i, id, target_i, assay_i, quantity, status, record) "
314
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
315
+ (reference_i, observation.measurement_id, target_i, assay_i,
316
+ observation.quantity, observation.status,
317
+ _json(_measurement_record(observation))))
318
+ connection.executemany("INSERT INTO observation_input VALUES (?, ?)",
319
+ ((cursor.lastrowid, input_i) for input_i in input_ids))
320
+ insert_target.cache_clear()
321
+ counts = {table: connection.execute(
322
+ f"SELECT count(*) FROM {'observation' if table == 'measurement' else table}").fetchone()[0]
265
323
  for table in ("sample", "assay", "mapping", "target", "measurement",
266
324
  "assay_pair", "target_relation", "reference_mapping")}
267
325
  connection.execute("INSERT INTO metadata VALUES ('counts', ?)", (_json(counts),))
326
+ # Real cardinalities prevent the planner from scanning a whole
327
+ # reference in ID order for a narrow target query.
328
+ connection.execute("ANALYZE")
268
329
  connection.commit()
269
330
  connection.close()
270
331
  connection = None
@@ -430,7 +491,8 @@ class QuantitativeDataset:
430
491
  filters = dict(reference_key=scope, target_key=_target_key(target) if target else None,
431
492
  sample_id=sample_id, assay_id=assay_id, quantity=quantity, status=status)
432
493
  selected = [(key, value) for key, value in filters.items() if value is not None]
433
- query = ("SELECT m.record, m.reference_key, t.kind, t.namespace, t.identifier "
494
+ query = ("SELECT m.record, m.reference_key, t.kind, t.namespace, t.identifier, "
495
+ "m.id, m.sample_id, m.assay_id, m.quantity, m.status "
434
496
  "FROM measurement m JOIN target t ON m.reference_key=t.reference_key "
435
497
  "AND m.target_key=t.key")
436
498
  if selected:
@@ -439,6 +501,11 @@ class QuantitativeDataset:
439
501
  with closing(self._connect()) as connection:
440
502
  for row in connection.execute(query, [value for _, value in selected]):
441
503
  values = json.loads(row[0])
504
+ if self._storage_version == 4:
505
+ values = dict(_V4_DEFAULTS, **values)
506
+ values.update(measurement_id=row[5], sample_id=row[6], assay_id=row[7],
507
+ quantity=row[8], status=row[9], dataset_id=self.manifest.dataset_id,
508
+ dataset_version=self.manifest.version)
442
509
  values["target"] = ScopedTarget(self._references[row[1]], row[2], row[4], row[3])
443
510
  if "input_references" in values:
444
511
  values["input_references"] = tuple(MeasurementReference.from_dict(value)
@@ -0,0 +1 @@
1
+ __version__ = "0.8.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orfdata
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Reference open reading frames and experimental translation evidence on annotated transcripts
5
5
  Author: OpenVax
6
6
  License-Expression: Apache-2.0
@@ -13,6 +13,7 @@ docs/pfizer-protein.md
13
13
  docs/reference.md
14
14
  docs/source-references.md
15
15
  docs/species.md
16
+ docs/storage.md
16
17
  orfdata/__init__.py
17
18
  orfdata/build.py
18
19
  orfdata/catalog.py
@@ -56,6 +57,7 @@ tests/test_pfizer.py
56
57
  tests/test_reference_context.py
57
58
  tests/test_reference_mapping.py
58
59
  tests/test_reference_orf.py
60
+ tests/test_storage.py
59
61
  tests/test_uorf_catalog.py
60
62
  tests/data/copy/GSM1905474.soft
61
63
  tests/data/copy/GSM1905474_KL9_fpkm.txt.gz
@@ -92,4 +94,12 @@ tests/data/references/canis_lupus_familiaris.json
92
94
  tests/data/references/context-keys-v0.4.json
93
95
  tests/data/references/homo_sapiens.json
94
96
  tests/data/references/mus_musculus.json
95
- tests/data/references/saccharomyces_cerevisiae.json
97
+ tests/data/references/saccharomyces_cerevisiae.json
98
+ tests/data/storage/README.md
99
+ tests/data/storage/generate.py
100
+ tests/data/storage/v1.json
101
+ tests/data/storage/v1.sqlite
102
+ tests/data/storage/v2.json
103
+ tests/data/storage/v2.sqlite
104
+ tests/data/storage/v3.json
105
+ tests/data/storage/v3.sqlite
@@ -0,0 +1,15 @@
1
+ These synthetic databases were generated by the **released** ORFdata versions
2
+ 0.3.0 (storage 1), 0.4.0 (storage 2) and 0.5.0 (storage 3), using `generate.py`.
3
+ They test actual legacy layouts; changing a new database's `user_version` would
4
+ not produce a valid old-format fixture.
5
+
6
+ Each contains four species with colliding local sample/assay/target/measurement
7
+ IDs, measured zero, a censored value, uncertainty and derived input lineage.
8
+ Versions 2/3 include pairings; version 3 includes reference mappings. No records
9
+ are experimental evidence. The JSON files retain the original writer's records.
10
+
11
+ Example regeneration from a checkout of the appropriate release:
12
+
13
+ ```sh
14
+ PYTHONPATH=/path/to/orfdata-0.3.0 python generate.py /path/to/fixtures 1
15
+ ```
@@ -0,0 +1,74 @@
1
+ """Run from released 0.3.0/0.4.0/0.5.0 to generate genuine legacy databases."""
2
+
3
+ import hashlib
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import orfdata
9
+ from orfdata import (
10
+ Assay,
11
+ DatasetManifest,
12
+ Measurement,
13
+ QuantitativeDataset,
14
+ ReferenceContext,
15
+ Sample,
16
+ ScopedTarget,
17
+ SourceAsset,
18
+ )
19
+
20
+ output = Path(sys.argv[1])
21
+ version = int(sys.argv[2])
22
+ references = tuple(ReferenceContext(taxon, name, assembly, "ensembl", "110")
23
+ for taxon, name, assembly in ((9606, "human", "GRCh38"),
24
+ (10090, "mouse", "GRCm39"),
25
+ (9615, "dog", "ROS_Cfam_1.0"),
26
+ (4932, "yeast", "R64-1-1")))
27
+ source = SourceAsset("input.tsv", "https://example.invalid/storage-fixture.tsv",
28
+ hashlib.sha256(b"synthetic fixture\n").hexdigest(), 18,
29
+ "Synthetic test data; Apache-2.0", redistributable=True)
30
+ manifest = DatasetManifest("legacy-fixture", "1", "Synthetic legacy storage validation",
31
+ references, (source,), ("Synthetic, not experimental evidence",),
32
+ ("rna_seq", "digest_ms"), {"scope": "four deliberately colliding species"},
33
+ {"writer": "orfdata", "writer_version": orfdata.__version__})
34
+ samples, assays, observations, pairs, relations, mappings = [], [], [], [], [], []
35
+ for index, reference in enumerate(references):
36
+ samples.append(Sample("sample", reference, biological_unit_id="culture"))
37
+ assays.extend((Assay("rna", "sample", "rna_seq", reference), Assay("protein", "sample", "digest_ms", reference)))
38
+ gene = ScopedTarget(reference, "gene", "same-id", "fixture")
39
+ protein = ScopedTarget(reference, "protein", "same-id", "fixture")
40
+ observations.extend((
41
+ Measurement("rna-value", gene, "sample", "rna", "rna_abundance", index, "TPM",
42
+ manifest.dataset_id, manifest.version, provenance={"source_asset": "input.tsv", "row": index}),
43
+ Measurement("protein-value", protein, "sample", "protein", "protein_abundance",
44
+ None if index == 1 else 10.25 + index, "iBAQ", manifest.dataset_id, manifest.version,
45
+ status="below_detection" if index == 1 else "measured", detection_limit=20 if index == 1 else None,
46
+ uncertainty=0.5, uncertainty_type="standard_deviation",
47
+ provenance={"source_asset": "input.tsv", "row": index, "native": "<20" if index == 1 else "observed"}),
48
+ Measurement("derived", gene, "sample", "rna", "rna_abundance", 1, "fixture-units",
49
+ manifest.dataset_id, manifest.version, origin="derived", input_measurements=("rna-value",),
50
+ derivation={"method": "synthetic lineage fixture"}),
51
+ ))
52
+ if version >= 2:
53
+ from orfdata import AssayPair, TargetRelation
54
+
55
+ pairs.append(AssayPair("pair", reference, manifest.dataset_id, manifest.version,
56
+ "rna", "protein", "same_material", {"source_asset": "input.tsv"}, "concurrent"))
57
+ relations.append(TargetRelation("relation", manifest.dataset_id, manifest.version, gene, protein,
58
+ "canonical_translation", "one_to_one", {"source_asset": "input.tsv"}))
59
+ if version >= 3:
60
+ from orfdata import ReferenceMapping
61
+
62
+ mappings.append(ReferenceMapping("map", manifest.dataset_id, manifest.version,
63
+ protein, reference, (ScopedTarget(reference, "protein", "mapped-id", "fixture"),),
64
+ "unique", "synthetic identifier match", {"sequence": "not validated"},
65
+ provenance={"source_asset": "input.tsv"}))
66
+ kwargs = dict(samples=samples, assays=assays, measurements=observations)
67
+ if version >= 2:
68
+ kwargs.update(pairings=pairs, relations=relations)
69
+ if version >= 3:
70
+ kwargs.update(reference_mappings=mappings)
71
+ dataset = QuantitativeDataset.create(output / f"v{version}.sqlite", manifest, **kwargs)
72
+ expected = dict(writer_version=orfdata.__version__, manifest=manifest.to_dict(),
73
+ measurements=[m.to_dict() for m in observations], counts=dataset.counts)
74
+ (output / f"v{version}.json").write_text(json.dumps(expected, indent=2) + "\n")