waxsql 1.0.0__py3-none-any.whl

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.
waxsql/__init__.py ADDED
@@ -0,0 +1,158 @@
1
+ """waxsql — random PostgreSQL query generator (wax-fruit SQL).
2
+
3
+ Public API surface at 1.0: the schema model, the type system, the
4
+ catalog, the validation modes (SYNTAX / PARSE / PLAN), and the
5
+ query generator.
6
+
7
+ The two headline functions:
8
+
9
+ * `generate_schema(seed, complexity)` — random Schema, deterministic
10
+ in seed.
11
+ * `generate_query(seed, schema, complexity)` — random Query against
12
+ that schema, deterministic in seed (independent RNG stream from
13
+ the schema generator's, so "same schema, different queries"
14
+ works by varying just the query seed).
15
+
16
+ Render queries with `print_query(q)`. Validate at three tiers:
17
+ `waxsql.validate.syntax.check_syntax(sql)` (no DB),
18
+ `waxsql.validate.parse.check_parse(sql, conn)` (live PG, PREPARE),
19
+ or `waxsql.validate.plan.check_plan(sql, conn)` (live PG, EXPLAIN).
20
+
21
+ Role in the system: this module IS the public surface. The `__all__`
22
+ at the bottom is the supported API contract; anything reachable only
23
+ via dotted paths into submodules is implementation detail subject to
24
+ churn. The data generator (`generate_data`) is re-exported here so
25
+ callers don't need to know it lives under `waxsql.data`.
26
+
27
+ Deliberately kept out of `__all__` to hold a small, stable 1.0 contract:
28
+
29
+ * The full AST node vocabulary (`ColumnRef`, `FuncCall`, `Subquery`,
30
+ `WindowSpec`, `CteDef`, ...) lives in `waxsql.ast` — the canonical
31
+ AST module. Import from there to construct or walk a query tree.
32
+ Only `Query` (the `generate_query` return type) and the `Select` /
33
+ `SetOp` shapes its `.select` can hold are surfaced here.
34
+ * The query-generator internals (`GenContext`, `Scope`, `Binding`,
35
+ `ComplexityConfig`, `query_config_for_complexity`, the `FEATURE_*`
36
+ flags) live in `waxsql.context` / `waxsql.scope` / `waxsql.config`.
37
+ They drive generation but are not a stable extension API — and the
38
+ functions that consume them (`gen_select` / `gen_setop`) aren't
39
+ public either, so exporting the context types alone would be
40
+ incoherent.
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import random
45
+ from importlib.metadata import PackageNotFoundError, version
46
+ from typing import Optional
47
+
48
+ # Only the workflow-facing AST types are surfaced at top level; the rest
49
+ # of the node vocabulary stays in `waxsql.ast` (see module docstring).
50
+ from .ast import Query, Select, SetOp
51
+ from .catalog import Catalog, FuncKind, FuncSig, OpSig, default_catalog
52
+ # query_config_for_complexity and FEATURE_SET_OP are imported for use in
53
+ # generate_query's body below; they are intentionally NOT re-exported
54
+ # (generator internals — see module docstring).
55
+ from .config import FEATURE_SET_OP, query_config_for_complexity
56
+ from .context import GenContext
57
+ from .data import generate_data
58
+ from .gen import gen_select, gen_setop
59
+ from .printer import print_expr, print_query
60
+ from .schema import (
61
+ Column, ForeignKey, Index, Schema, SchemaConfig, Table,
62
+ generate_schema, generate_schema_with_config,
63
+ quote_ident, schema_config_for_complexity,
64
+ )
65
+ from .scope import Scope
66
+ from .types import (
67
+ BOOL, DATE, FLOAT8, INT4, INT8, INTERVAL, JSONB, NUMERIC,
68
+ PgType, SCALAR_TYPES, TEXT, TIMESTAMPTZ, TypeCategory,
69
+ UUID, VARCHAR, array_of, implicitly_castable,
70
+ )
71
+ from .validate import ValidationMode
72
+
73
+ # Version is the single source of truth in pyproject.toml. Read it at
74
+ # import time via importlib.metadata so we never have two constants to
75
+ # keep in sync. The PackageNotFoundError fallback handles the rare
76
+ # "running directly from the source tree without an install" case.
77
+ try:
78
+ __version__ = version("waxsql")
79
+ except PackageNotFoundError: # pragma: no cover
80
+ __version__ = "0.0.0+unknown"
81
+
82
+
83
+ def generate_query(
84
+ seed: int,
85
+ *,
86
+ schema: Schema,
87
+ complexity: int = 5,
88
+ catalog: Optional[Catalog] = None,
89
+ ) -> Query:
90
+ """Generate a random Query against `schema`, deterministic in `seed`.
91
+
92
+ Same `(seed, schema, complexity, catalog)` always produces an
93
+ identical Query (and identical printed SQL). The query-generator
94
+ RNG is independent of the schema-generator RNG — pass the same
95
+ seed to both for "fully reproducible session", or vary the query
96
+ seed to fuzz queries against a fixed schema.
97
+
98
+ `catalog` defaults to `default_catalog()` if not supplied.
99
+ """
100
+ cat = catalog if catalog is not None else default_catalog()
101
+ cfg = query_config_for_complexity(complexity)
102
+ # Construct the GenContext exactly once here, at the entry point.
103
+ # Every downstream generator gets a derivative of this ctx via
104
+ # GenContext.descend / descend_subquery / dataclasses.replace —
105
+ # and crucially, every derivative shares this rng instance. That
106
+ # shared mutable rng is what makes (seed, schema, complexity) ↦
107
+ # query a deterministic function: reordering or duplicating the
108
+ # rng-consuming calls anywhere in the generator tree perturbs the
109
+ # output. Resist any urge to fork a fresh Random() inside a
110
+ # downstream generator "for cleanliness"; it would silently break
111
+ # the determinism test and the fuzzer's bug-reproduction guarantee.
112
+ ctx = GenContext(
113
+ rng=random.Random(seed),
114
+ scope=Scope(),
115
+ schema=schema,
116
+ catalog=cat,
117
+ config=cfg,
118
+ depth_remaining=cfg.max_expr_depth,
119
+ # Opt in to the subquery budget; the GenContext default is 0
120
+ # (no subqueries) so callers must explicitly enable them.
121
+ subquery_depth_remaining=cfg.max_subquery_depth,
122
+ )
123
+ # Top-level dispatch: SetOp (UNION/INTERSECT/EXCEPT) when the
124
+ # feature is unlocked AND the dice roll fires; plain Select
125
+ # otherwise. Set-ops live at the OUTERMOST level — the generator
126
+ # doesn't produce set ops inside subqueries or CTE bodies.
127
+ body: Select | SetOp
128
+ if (FEATURE_SET_OP in cfg.feature_flags
129
+ and ctx.rng.random() < cfg.p_set_op_query):
130
+ body = gen_setop(ctx)
131
+ else:
132
+ body = gen_select(ctx)
133
+ return Query(select=body)
134
+
135
+
136
+ __all__ = [
137
+ # Types
138
+ "PgType", "TypeCategory",
139
+ "INT4", "INT8", "NUMERIC", "FLOAT8", "TEXT", "VARCHAR", "BOOL",
140
+ "DATE", "TIMESTAMPTZ", "INTERVAL", "UUID", "JSONB",
141
+ "SCALAR_TYPES", "array_of", "implicitly_castable",
142
+ # Schema
143
+ "Schema", "Table", "Column", "ForeignKey", "Index",
144
+ "SchemaConfig", "schema_config_for_complexity",
145
+ "generate_schema", "generate_schema_with_config",
146
+ "quote_ident",
147
+ # Catalog
148
+ "Catalog", "FuncSig", "OpSig", "FuncKind", "default_catalog",
149
+ # Validation
150
+ "ValidationMode",
151
+ # Query result type + the two shapes its `.select` can hold. The rest
152
+ # of the AST node vocabulary lives in `waxsql.ast` (see docstring).
153
+ "Query", "Select", "SetOp",
154
+ # Generation + rendering
155
+ "generate_query", "print_query", "print_expr",
156
+ # Data generator
157
+ "generate_data",
158
+ ]