Skip to content

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 19:53
· 19 commits to main since this release
8968cce

Added

  • Paired fills the second key of a many-to-many, and the edge count stays
    exact.
    Two fan-outs on one through table are refused because a fan-out is a
    partition computed from the row index alone, so two of them partition the same
    rows without either seeing the other -- nothing enumerates the pairs and a
    collision is a matter of the seed. person=Paired("company", Zipf()) is the
    half that looks: company partitions the rows as any fan-out does, and within
    each of its groups person takes that many distinct partners. A duplicate
    pair is impossible by construction rather than removed afterwards, so nothing
    is deduplicated and the build never reports an achieved count against a
    requested one. The design notes for this milestone had given that up; they
    were wrong, and the rule that cardinality is declared survives M2M.

    • What binds is the busiest group, not the product. Every row of one
      group needs a different partner, so the constraint is
      largest group <= partners rather than rows <= groups x partners -- and a
      heavy tail puts a large share of every edge on one group, Zipf(1.2) over
      five thousand of them putting 21% on the top one. A declaration that looks
      sparse against the product can still be impossible, and it cannot be known
      until the partition is resolved: this is the one structural refusal that
      waits for the build rather than firing where the shape is written.
    • The second side is derived and approximate, and the numbers are
      published.
      Both marginals plus the edge count is over-determined, and
      fixing all three is a constraint satisfaction problem that cannot stream
      into COPY. Measured against exact weighted sampling over 200,000 edges,
      the busiest partner comes out 1.09 times as busy, the 99th percentile about
      30% high, and about 7% fewer partners are touched.
    • Partners are allocated across weight bands and strided within one, so
      nothing is ever drawn and asked whether it is taken. Rejection sampling
      degenerates exactly where the shape is most interesting -- 245 probes per
      row on that world -- and the usual fix, sampling the ones to leave out when
      a group wants more than half, is ten times faster and a different sampling
      rule
      , so a group one row over the halfway mark would get a materially
      different membership from one row under. The band count is derived from
      the declared weights
      rather than chosen, which systematic allocation is
      what makes possible: it converges as bands get finer where largest-remainder
      runs away, so "enough" is a limit rather than a number somebody tuned.
  • FanOutPlan.group_of answers which parent a row belongs to, which a
    pairing needs to choose partners inside this row's group.

  • Projection(..., through=Model) says which model a derived join runs on.
    An abstract base carrying created_by/updated_by to User -- a pattern most
    Django schemas have -- makes any two models in the schema joinable, so the
    derivation had candidates everywhere and could settle nothing: not for one
    pair but for every pair, which put the derived form out of reach for a whole
    application and left sql= as the only route to this feature's own motivating
    example. The refusal now names through= first, and names a model that can
    actually resolve the join rather than the first one alphabetically -- an audit
    model is reached by two edges from each side, so it is a real candidate and
    never a usable answer. Where every candidate is reached more than once,
    through= cannot narrow anything and the refusal says so instead of
    suggesting it.

Changed

  • The two-fan-out refusal names the declaration that replaces it. Its remedy
    used to be a statement of your own, because nothing else could keep the
    constraint. It now names Paired first and keeps Projection(columns=, sql=)
    as the escape hatch for an edge set that has to be a particular one.

Fixed

  • A rounded Uniform can size a fan-out.
    FanOut(Uniform(1, 10, places=0)) was refused with "needs numeric fan-out
    sizes, but produced Decimal('3')" -- which reads as a contradiction, and was
    one keyword away from FanOut(Uniform(1, 10)), the spelling this package's own
    docstring recommends and two of its refusals suggest. places=0 is the natural
    way to say that a fan-out size is a count. The guard was
    isinstance(weight, (int, float)) while the next line already did
    float(weight), so it was stricter than the arithmetic it protected. It is now
    the numeric tower plus Decimal, which numbers.Real deliberately excludes,
    and Fraction and numpy scalars come along with it. bool stays refused. The
    message also names a remedy now, which it was alone in not doing.

  • columns= accepts the column an INSERT actually lists. The refusal that
    sends a reader there says "the select's columns are what the insert lists", and
    what an insert lists for a foreign key is event_id -- which was then refused
    as "no field named event_id". That is a contradiction rather than a correction,
    and the surrounding documentation reinforced the wrong reading. Both spellings
    are accepted and resolve to the same column, the messages say so, and the same
    dict is read for statistics= so the two cannot disagree about what a column
    is called.