@@ -135,6 +135,141 @@ openstatspec apply-spss --database-url sqlite:///survey.sqlite \
135135
136136On Dolt, also pass ` --expected-branch ` and ` --expected-head ` .
137137
138+ ## Numeric targets: create versus replace
139+
140+ ` COMPUTE ` chooses its canonical target mode from the live schema. An absent
141+ numeric target lowers to ` assign ` with ` target_mode=create ` ; it appends one
142+ nullable numeric column and one catalog variable. An existing numeric target
143+ lowers to ` target_mode=replace ` . Replace keeps the variable ID, ordinal, and
144+ metadata that no later operation explicitly changes. ` IF ` requires an existing
145+ target, so a new target must be initialized first. Variable-source ` NULL `
146+ propagates through assignment; a predicate changes a row only when its SQL
147+ truth value is ` TRUE ` .
148+
149+ For example, this is a create-or-replace program for a numeric target. On
150+ SQLite/PostgreSQL it creates ` target ` when absent; with a pre-existing numeric
151+ ` target ` it updates the same target instead:
152+
153+ ``` text
154+ COMPUTE target = source_a.
155+ IF (source_a = 1 AND source_b = 1) target = 1.
156+ IF (target = 1 AND source_b = 1) target = 2.
157+ VARIABLE LABELS target 'Numeric target'.
158+ VALUE LABELS target 0 'No' 1 'Yes'.
159+ FORMATS target (F8.1).
160+ VARIABLE LEVEL target (NOMINAL).
161+ EXECUTE.
162+ ```
163+
164+ ` RECODE ... INTO ` always requests a new target and rejects an existing target
165+ name. A canonical replace recode must use the same source and target name.
166+ Without ` ELSE ` , create recodes use ` system_missing ` for unmatched rows, while
167+ replace recodes use ` copy ` . To pre-provision ` band ` and preserve the create
168+ semantics, initialize and recode it explicitly. The absent-target form is:
169+
170+ ``` text
171+ RECODE source_a (1 = 0.1) (2 = 1) INTO band.
172+ ```
173+
174+ The pre-existing-target form is:
175+
176+ ``` text
177+ COMPUTE band = source_a.
178+ RECODE band (1 = 0.1) (2 = 1) (ELSE = SYSMIS).
179+ ```
180+
181+ The second form has an extra ordered assignment and is not a pure DDL-overhead
182+ comparison. Omitting its ` ELSE ` would copy unmatched source values instead of
183+ matching the create form. For canonical plans, create recodes use
184+ ` target_mode=create ` and replace recodes use ` source=target ` ,
185+ ` target_mode=replace ` .
186+
187+ On SQLite and PostgreSQL, numeric create is supported only where the adapter
188+ has the atomic native transaction boundary. MySQL, MariaDB, and Dolt reject a
189+ create target with ` schema_change_not_atomic ` . A caller must use a separate,
190+ explicit, versioned provisioning action that creates both the nullable physical
191+ column and its matching normative variable, including a unique variable ID,
192+ the same physical binding, and the next ordinal. Provisioning is not an apply
193+ step, and this package does not hide it behind a new helper or API. See the
194+ specification's [ In-Place Transformation Binding 0.2 target-creation
195+ rules] ( https://github.com/OpenStatSpec/specification/blob/main/docs/transformation-plan-sql-binding-0.2.md#target-creation-by-sql-profile ) .
196+ For Dolt, commit provisioning separately; the later apply starts from that
197+ clean committed ` HEAD ` and supplies it as ` expected_head ` . The SQLite matrix
198+ below is local evidence only and is not service or cross-engine evidence.
199+
200+ ## Local numeric-target measurement
201+
202+ The following is a bounded observation of the existing workflow, not a
203+ release-to-release benchmark. A disposable standard-library harness created a
204+ fresh file-backed SQLite database for every variant and repetition, kept
205+ fixture setup separate from the timed public call, and removed all temporary
206+ files afterward. It covered two workloads (A: the ` COMPUTE ` /` IF ` program above;
207+ B: ` RECODE ` ), create versus pre-provisioned replace, 100 versus 100,000 cases,
208+ and both ` apply_transformation_plan_in_place ` and ` apply_spss_in_place ` . Each
209+ cell had one counted fresh apply, one fresh warm-up, and five fresh timed
210+ applies; create/replace order alternated by repetition. Each counted,
211+ warm-up, and timed apply started from a newly created fixture database; no
212+ apply reused a database from another trial. Rows were generated by cycling
213+ this input sequence in order: ` (1,1) ` , ` (1,NULL) ` , ` (NULL,1) ` , ` (0,1) ` ,
214+ ` (2,2) ` , ` (0.1,0) ` , ` (-9,1) ` , ` (NULL,0) ` . Thus the 100-row runs used 12 full
215+ cycles plus the first four rows of a cycle, while the 100,000-row runs used
216+ 12,500 full cycles. Each fixture had numeric ` source_a ` and ` source_b ` ; ` source_a ` carried
217+ label ` Source A ` and missing rule ` 99 ` , while ` source_b ` carried label
218+ ` Source B ` and no missing rule. Replace fixtures added one nullable numeric
219+ ordinal-3 target (` target ` for A, ` band ` for B) with label ` Old target ` , ` F4.0 `
220+ formats, ordinal measurement level, and missing rule ` 99 ` ; create fixtures had
221+ no target column.
222+
223+ The counted value is an outermost SQLAlchemy ` Connection.execute ` or
224+ ` Connection.exec_driver_sql ` invocation scoped to the owned database. It
225+ includes profile probes, catalog checks, reflection, explicit ` BEGIN ` , DDL,
226+ DML, and audit work; one executemany call counts once. It is not a driver wire
227+ round-trip count. Timed values are the public apply call only: setup,
228+ validation, and cleanup are excluded. Every counted and timed result checked
229+ ordered rows, explicit ` NULL ` s, binary64 bits (including ` 0.1 ` ), source and
230+ target identity, catalog/audit provenance, and absence of forbidden copy or
231+ snapshot artifacts before being retained.
232+
233+ Environment and revisions: Python 3.13.2, SQLite 3.47.1, SQLAlchemy 2.0.52,
234+ Linux 6.6.87.2-microsoft-standard-WSL2 on x86_64 with 12 logical CPUs and
235+ local temporary-file storage; observed file settings were
236+ ` journal_mode=delete ` and ` synchronous=2 ` (` FULL ` ). The Python source was
237+ ` 63a66d6589aeb8fdc4820d5e7217fc13c61ef3b9 ` ; the reviewed specification
238+ reference was ` 930345b922af4f43b0e622be02d4b12ebfeb08eb ` . Fixture setup was
239+ recorded separately and excluded from the table.
240+
241+ | Workload | Cases | API | Mode | SQLAlchemy executes | Median ms | Min ms | Max ms |
242+ | --- | ---: | --- | --- | ---: | ---: | ---: | ---: |
243+ | A | 100 | canonical plan | create | 234 | 76.5 | 65.8 | 78.4 |
244+ | A | 100 | SPSS | create | 237 | 69.3 | 66.8 | 78.2 |
245+ | A | 100 | canonical plan | replace | 222 | 67.6 | 56.6 | 88.3 |
246+ | A | 100 | SPSS | replace | 225 | 71.3 | 69.3 | 88.5 |
247+ | A | 100,000 | canonical plan | create | 234 | 168.8 | 134.1 | 172.3 |
248+ | A | 100,000 | SPSS | create | 237 | 160.2 | 149.4 | 170.3 |
249+ | A | 100,000 | canonical plan | replace | 222 | 162.7 | 146.0 | 173.6 |
250+ | A | 100,000 | SPSS | replace | 225 | 155.4 | 144.8 | 177.6 |
251+ | B | 100 | canonical plan | create | 224 | 61.6 | 55.6 | 76.6 |
252+ | B | 100 | SPSS | create | 227 | 77.1 | 65.5 | 312.8 |
253+ | B | 100 | canonical plan | replace | 213 | 64.2 | 60.2 | 223.1 |
254+ | B | 100 | SPSS | replace | 216 | 80.9 | 62.2 | 237.5 |
255+ | B | 100,000 | canonical plan | create | 224 | 149.4 | 131.7 | 193.4 |
256+ | B | 100,000 | SPSS | create | 227 | 144.1 | 128.3 | 158.4 |
257+ | B | 100,000 | canonical plan | replace | 213 | 169.8 | 163.6 | 186.6 |
258+ | B | 100,000 | SPSS | replace | 216 | 192.8 | 171.4 | 202.8 |
259+
260+ The canonical plan operation/hash identities for the same runs were:
261+
262+ | Workload | Mode | Operations | Contract | Canonical plan SHA-256 |
263+ | --- | --- | ---: | --- | --- |
264+ | A | create | 8 | v0.2 | ` 6b17a220f9be9d2d29b671dd5eb4598ea34ae459e4d3b8e84a7daafbe3ec11ef ` |
265+ | A | replace | 8 | v0.2 | ` a239cac39d3fade1207228bee4296a444669eb31e154e6cea09f378ea75b12eb ` |
266+ | B | create | 1 | v0.1 | ` 2057e57117100ba3efd5becf5254f47a5696bc8631c35480a5a6d2f5b1327f83 ` |
267+ | B | replace | 2 | v0.2 | ` 23b4bbf06383c5e73e04356af079db1e6fe0827f8afdb406f849169306fa2214 ` |
268+
269+ These observations do not establish service latency, wire round trips,
270+ row-independent cost, or a before/after speedup for any release. They are
271+ owned SQLite evidence for the documented behavior at the revisions above.
272+
138273## Database and Dolt invariants
139274
140275Every successful apply preserves the logical ` dataset_id ` and physical
0 commit comments