Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cf1dd9f
fix(types): refuse a non-boolean unsigned when the type is rendered
vistart Oct 11, 2026
cc9c051
refactor(bigquery): read declared fields directly instead of probing …
vistart Oct 11, 2026
b8beb95
refactor(bigquery): follow the statement and expression renames
vistart Oct 14, 2026
af92c97
refactor(bigquery): migrate constraints onto the two-level branch nodes
vistart Oct 14, 2026
5997d41
fix(bigquery): converge the constraint switches onto the (self) probe…
vistart Oct 14, 2026
de7e96a
refactor(bigquery): split the wildcard by grammar position
vistart Oct 14, 2026
b3015a2
test(bigquery): reconcile the roundtrip matrix with the new core nodes
vistart Oct 14, 2026
6011a99
ci: point testsuite at the matching branch
vistart Oct 14, 2026
20eae67
refactor(expression): follow the core renames and the split registry
vistart Oct 16, 2026
039e20d
refactor(bigquery): read declared fields instead of probing with getattr
vistart Oct 17, 2026
391469f
feat(bigquery): adopt the typed value nodes with BigQuery literal spe…
vistart Oct 17, 2026
bd22d12
test(bigquery): reconcile the roundtrip matrix with the value and fun…
vistart Oct 18, 2026
9b86d32
refactor(bigquery): follow the literal architecture the core settled
vistart Oct 20, 2026
172c67f
refactor(bigquery): audit the kinds this backend overrides and owns
vistart Oct 20, 2026
1342553
refactor(bigquery): follow the rules the org settled for kinds and ov…
vistart Oct 20, 2026
e37fa4b
test(bigquery): move live-connection checks out of expression tests
vistart Oct 20, 2026
1493c7a
test(bigquery): reconcile the roundtrip matrix with the core's curren…
vistart Oct 20, 2026
231ab2c
refactor(bigquery): build the backend's own probes from expression nodes
vistart Oct 20, 2026
c4f3bff
fix(bigquery): refuse the ILIKE spelling the grammar does not have
vistart Oct 20, 2026
92caf6c
test(bigquery): drop the codec pins the core fix retired
vistart Oct 20, 2026
1d0026d
chore: ignore .worktree/, the branch toolchain directories
vistart Oct 21, 2026
074be7f
fix(bigquery): the dialect surface, and the type positions it decides
vistart Oct 22, 2026
5ffc318
refactor(expression)!: follow core's statement rename
vistart Oct 22, 2026
a69ce30
refactor(expression)!: follow the renamed core nodes and formatters
vistart Oct 23, 2026
3c04003
refactor(backend)!: call the core helpers by their public names
vistart Oct 23, 2026
1ab01e1
feat(introspection): answer the metadata questions from INFORMATION_S…
vistart Oct 23, 2026
6ffe66d
docs(cli): say what introspect can ask about
vistart Oct 24, 2026
6c870b1
refactor(cli): name the namespace after the engine, not after the slot
vistart Oct 24, 2026
c7eb3eb
feat(introspection): declare BigQuery's dataset family
vistart Oct 24, 2026
4cac797
refactor(bigquery)!: drop the underscores from the introspection cont…
vistart Oct 25, 2026
1f5b8df
refactor(bigquery): name the catalogue questions as statement factories
vistart Oct 25, 2026
62e0b45
feat(cli)!: the introspect table command is gone
vistart Oct 26, 2026
579e18d
fix(bigquery): stop claiming an index catalogue that does not exist
vistart Oct 27, 2026
ea4588c
ci: install core from the release branch now that #127 has merged
vistart Oct 30, 2026
4ab71cb
docs(changelog): the fragments for #5
vistart Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ permissions:

env:
# rhosocial-activerecord and rhosocial-activerecord-testsuite dev releases are
# not published to PyPI; install them from the release branches.
# not published to PyPI; install them from git.
# The schema-object refactor this branch depends on was developed on the core
# library's expression-schema-object-refactor branch and merged to
# release/v1.0.0.dev30 (PR #125), which is the ref CORE_PKG pins. Two of the
# release/v1.0.0.dev30 (PR #125). This branch now pins CORE_PKG at its own
# development branch -- the constraint-gates work it carries is not on any
# release branch yet -- and pins TESTSUITE_PKG at the matching branch for the
# same reason, so the two move together. Two of the
# four names a previous revision of this comment listed -- QualifiedNameMixin
# and CatalogSupport -- are ones that work *deleted*, along with the
# single-`supports_schema_qualification` object model this backend used to
Expand All @@ -42,7 +45,7 @@ env:
# of that layer, so the imports this backend now makes do not resolve against
# it.
CORE_PKG: "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
TESTSUITE_PKG: "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
TESTSUITE_PKG: "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@fix/column-constraint-gates"

jobs:
test:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ coverage.xml

# Local PR drafts and other scratch files used by the merge flow
.claude/tmp*/
# Worktrees for feature/* and fix/* branches. Never tracked: a `git add -A`
# from the repo root would commit each branch's whole toolchain -- the venv, the
# build directory and the test artifacts -- and the directory disappears when the
# branch merges, so the history would keep a second copy of the source tree.
.worktree/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ from typing import ClassVar, Optional
from rhosocial.activerecord.base import FieldProxy
from rhosocial.activerecord.backend.impl.bigquery.backend import BigQueryBackend
from rhosocial.activerecord.backend.impl.bigquery.config import BigQueryConnectionConfig
from rhosocial.activerecord.backend.expression import CreateTableExpression
from rhosocial.activerecord.backend.expression import CreateTableStatement
from rhosocial.activerecord.backend.expression.statements import ColumnDefinition
from rhosocial.activerecord.backend.expression.types import IntegerType, VarCharType
from rhosocial.activerecord.model import ActiveRecord
Expand All @@ -73,7 +73,7 @@ User.configure(
)

dialect = User.__backend__.dialect
User.__backend__.execute(*CreateTableExpression(
User.__backend__.execute(*CreateTableStatement(
dialect,
"users",
columns=[
Expand Down
2 changes: 1 addition & 1 deletion changelog.d/3.added.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Add BigQuery materialized view support: `BigQueryCreateMaterializedViewExpression` (`OR REPLACE` / `IF NOT EXISTS` / `PARTITION BY` / `CLUSTER BY` / `OPTIONS(...)`), `BigQueryDropMaterializedViewExpression`, `BigQueryAlterMaterializedViewSetOptionsExpression`, `BigQueryCreateMaterializedViewReplicaExpression` (`AS REPLICA OF`), and the enumerated `BigQueryMaterializedViewOption` catalogue for `OPTIONS(...)` names. Generic-expression clauses BigQuery does not have (column aliases, `TABLESPACE`, storage parameters, `WITH [NO] DATA`, `CASCADE`) are rejected with `UnsupportedFeatureError` rather than silently dropped. `supports_materialized_view()` now reports `True`; `supports_refresh_materialized_view()` stays `False` because BigQuery has no `REFRESH MATERIALIZED VIEW` statement — refresh is scheduled through `OPTIONS(enable_refresh=…, refresh_interval_minutes=…)`.
Add BigQuery materialized view support: `BigQueryCreateMaterializedViewStatement` (`OR REPLACE` / `IF NOT EXISTS` / `PARTITION BY` / `CLUSTER BY` / `OPTIONS(...)`), `BigQueryDropMaterializedViewStatement`, `BigQueryAlterMaterializedViewSetOptionsStatement`, `BigQueryCreateMaterializedViewReplicaStatement` (`AS REPLICA OF`), and the enumerated `BigQueryMaterializedViewOption` catalogue for `OPTIONS(...)` names. Generic-expression clauses BigQuery does not have (column aliases, `TABLESPACE`, storage parameters, `WITH [NO] DATA`, `CASCADE`) are rejected with `UnsupportedFeatureError` rather than silently dropped. `supports_materialized_view()` now reports `True`; `supports_refresh_materialized_view()` stays `False` because BigQuery has no `REFRESH MATERIALIZED VIEW` statement — refresh is scheduled through `OPTIONS(enable_refresh=…, refresh_interval_minutes=…)`.
1 change: 1 addition & 0 deletions changelog.d/5.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The dataset family, the metadata questions answered from `INFORMATION_SCHEMA`, and statement factories for the catalogue reads. The introspect type list derives from `_TYPE_CAPABILITIES`.
1 change: 1 addition & 0 deletions changelog.d/5.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`introspect table` is gone, and the catalogue questions are named as statement factories under the engine's own namespace.
1 change: 1 addition & 0 deletions changelog.d/5.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The backend no longer claims an index catalogue it does not have, and the `ILIKE` spelling its grammar lacks is refused.
1 change: 1 addition & 0 deletions changelog.d/5.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The round-trip matrix follows the core's current tree, and CI installs core from `release/v1.0.0.dev30` now that core #127 has merged.
98 changes: 49 additions & 49 deletions docs/en_US/bigquery_specific_features/schema_namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,34 +179,33 @@ documented table-name forms are `mytable`, `dataset.mytable` and
[from-clause]: https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#from_clause

`BigQueryDialect` enforces this by overriding `format_column` rather than
letting the core renderer qualify the column, and `format_wildcard` does the
same for `table.*`:
letting the core renderer qualify the column; the wildcard itself is core's
default spelling, and `format_all_columns_wildcard` does the same for
`table.*` (the qualifier rides the relation object, so what is rendered is the
name the query itself uses):

```python
Column(d, "id", table="orders").to_sql()[0]
# `orders`.`id`

WildcardExpression(d, table="orders").to_sql()[0]
AllColumnsWildcard(d, relation=NamedRelationRef(d, Table(d, "orders"))).to_sql()[0]
# `orders`.*

WildcardExpression(d).to_sql()[0]
AllColumnsWildcard(d).to_sql()[0]
# *
```

`format_wildcard` is the one formatter that discards a `schema_name` without
rendering it and without validating it, an empty string included:
The asterisk is split by grammar position (spec 4.10): the select list and
RETURNING take `AllColumnsWildcard`, and `COUNT`'s argument takes
`RowCountWildcard`. The two are not interchangeable. An
`AllColumnsWildcard`'s relation is a `NamedRelationRef`, which carries both the
name and the qualifier, so the alias a query uses is the alias rendered --
BigQuery need not discard or validate a dataset here.

```python
WildcardExpression(d, table="orders", schema_name="app").to_sql()[0]
# `orders`.*

WildcardExpression(d, table="orders", schema_name="").to_sql()[0]
# `orders`.*
```

Nothing is lost from the output, since a wildcard carries no dataset to begin
with. The asymmetry is worth noting next to `format_column`, which does refuse an
empty dataset.
One BigQuery-specific asterisk spelling does **not** enter core: the wildcard
table (`dataset.*`). It is a dataset expansion in a Table position, not a
column expansion and not a row count, so it keeps its backend-side form rather
than being pressed onto either core wildcard node.

Two further consequences of the override are worth stating precisely, because
they differ from the core renderer. Both follow from one declared capability,
Expand Down Expand Up @@ -340,18 +339,18 @@ A CTE is named for the rest of the query, not for the database, so its own name
bare. The query inside it still carries the model's dataset:

```python
inner = QueryExpression(
inner = QueryStatement(
dialect=d,
select=[Column(d, "id", table="orders", schema_name="app")],
from_=[TableExpression(d, "orders", schema_name="app")],
)
main = QueryExpression(
main = QueryStatement(
dialect=d,
select=[Column(d, "id", table="recent_orders")],
from_=[TableExpression(d, "recent_orders")],
)

WithQueryExpression(d, [CTEExpression(d, "recent_orders", inner)], main).to_sql()[0]
WithStatement(d, [CTEClause(d, "recent_orders", inner)], main).to_sql()[0]
# WITH `recent_orders` AS (SELECT `orders`.`id` FROM `app`.`orders`)
# SELECT `recent_orders`.`id` FROM `recent_orders`
```
Expand Down Expand Up @@ -421,16 +420,16 @@ is built — a migration has to name the dataset it means — but every statemen
names a dataset-bearing object accepts a `schema_name` of its own:

```python
CreateTableExpression(
CreateTableStatement(
d, TableExpression(d, "orders", schema_name="app"), columns=cols
).to_sql()[0]
# CREATE TABLE `app`.`orders` (`id` INT64)

DropTableExpression(d, TableExpression(d, "orders", schema_name="app"),
DropTableStatement(d, TableExpression(d, "orders", schema_name="app"),
if_exists=True).to_sql()[0]
# DROP TABLE IF EXISTS `app`.`orders`

CreateViewExpression(d, "v_orders", query, schema_name="app").to_sql()[0]
CreateViewStatement(d, "v_orders", query, schema_name="app").to_sql()[0]
# CREATE VIEW `app`.`v_orders` AS SELECT `orders`.`id` FROM `app`.`orders`
```

Expand All @@ -441,18 +440,18 @@ Without a `WHERE` clause they render the range and nothing else, which is also
the shortest way to see that no dataset reaches a predicate:

```python
InsertExpression(
InsertStatement(
d, TableExpression(d, "users", schema_name="app"),
ValuesSource(d, [[Literal(d, "x")]]), columns=["name"],
).to_sql()
# ('INSERT INTO `app`.`users` (`name`) VALUES (?)', ('x',))

UpdateExpression(
UpdateStatement(
d, TableExpression(d, "users", schema_name="app"), {"name": Literal(d, "x")}
).to_sql()
# ('UPDATE `app`.`users` SET `name` = ?', ('x',))

DeleteExpression(d, [TableExpression(d, "users", schema_name="app")]).to_sql()
DeleteStatement(d, [TableExpression(d, "users", schema_name="app")]).to_sql()
# ('DELETE FROM `app`.`users`', ())
```

Expand All @@ -471,11 +470,11 @@ Two DDL statements are refused rather than qualified, because BigQuery does not
have the clause:

```python
DropViewExpression(d, "v_orders", schema_name="app", if_exists=True).to_sql()
DropViewStatement(d, "v_orders", schema_name="app", if_exists=True).to_sql()
# UnsupportedFeatureError: 'BigQuery' dialect does not support DROP VIEW IF
# EXISTS. Suggestion: BigQuery does not support DROP VIEW IF EXISTS.

CreateViewExpression(d, "v_orders", query, schema_name="app", if_not_exists=True).to_sql()
CreateViewStatement(d, "v_orders", query, schema_name="app", if_not_exists=True).to_sql()
# UnsupportedFeatureError: 'BigQuery' dialect does not support CREATE VIEW IF
# NOT EXISTS. Suggestion: BigQuery does not support CREATE VIEW IF NOT EXISTS.
```
Expand All @@ -488,25 +487,25 @@ Both statements are rendered through the dataset name as a single quoted
identifier:

```python
CreateSchemaExpression(d, "sales").to_sql()[0]
CreateSchemaStatement(d, "sales").to_sql()[0]
# CREATE SCHEMA `sales`

CreateSchemaExpression(d, "sales", if_not_exists=True).to_sql()[0]
CreateSchemaStatement(d, "sales", if_not_exists=True).to_sql()[0]
# CREATE SCHEMA IF NOT EXISTS `sales`

DropSchemaExpression(d, "sales", if_exists=True).to_sql()[0]
DropSchemaStatement(d, "sales", if_exists=True).to_sql()[0]
# DROP SCHEMA IF EXISTS `sales`
```

`authorization=` is refused, and `cascade=True` is refused as well:

```python
CreateSchemaExpression(d, "sales", authorization="app_user").to_sql()
CreateSchemaStatement(d, "sales", authorization="app_user").to_sql()
# UnsupportedFeatureError: 'BigQuery' dialect does not support CREATE SCHEMA
# AUTHORIZATION. Suggestion: BigQuery does not support CREATE SCHEMA
# AUTHORIZATION.

DropSchemaExpression(d, "sales", cascade=True).to_sql()
DropSchemaStatement(d, "sales", cascade=True).to_sql()
# UnsupportedFeatureError: 'BigQuery' dialect does not support DROP SCHEMA
# CASCADE. Suggestion: BigQuery does not support DROP SCHEMA CASCADE.
```
Expand Down Expand Up @@ -545,23 +544,23 @@ All four materialized view statements name their target as a
slot and the project in its `catalog_name`:

```python
BigQueryCreateMaterializedViewExpression(
BigQueryCreateMaterializedViewStatement(
d, MaterializedView("mv", schema_name="app"), query=inner,
).to_sql()[0]
# CREATE MATERIALIZED VIEW `app`.`mv` AS SELECT `orders`.`id` FROM `app`.`orders`

BigQueryDropMaterializedViewExpression(
BigQueryDropMaterializedViewStatement(
d, MaterializedView("mv", schema_name="app"), if_exists=True,
).to_sql()[0]
# DROP MATERIALIZED VIEW IF EXISTS `app`.`mv`

BigQueryAlterMaterializedViewSetOptionsExpression(
BigQueryAlterMaterializedViewSetOptionsStatement(
d, MaterializedView("mv", schema_name="app"),
options={"enable_refresh": True, "refresh_interval_minutes": 30},
).to_sql()[0]
# ALTER MATERIALIZED VIEW `app`.`mv` SET OPTIONS(enable_refresh = true, refresh_interval_minutes = 30)

BigQueryDropMaterializedViewExpression(
BigQueryDropMaterializedViewStatement(
d, MaterializedView("mv", schema_name="app", catalog_name="myproject"),
).to_sql()[0]
# DROP MATERIALIZED VIEW `myproject`.`app`.`mv`
Expand All @@ -571,7 +570,7 @@ Each slot is rendered where it belongs. A project with no dataset is refused,
because `project.table` is not a path BigQuery can resolve:

```python
BigQueryDropMaterializedViewExpression(
BigQueryDropMaterializedViewStatement(
d, MaterializedView("mv", catalog_name="myproject"),
).to_sql()
# ValueError: BigQuery renders a project only together with a dataset;
Expand All @@ -588,7 +587,7 @@ source choose their datasets independently — a replica normally sits in a
different dataset from the view it mirrors.

```python
BigQueryCreateMaterializedViewReplicaExpression(
BigQueryCreateMaterializedViewReplicaStatement(
d,
MaterializedView("mv_replica", schema_name="app"),
MaterializedView("mv_src", schema_name="s3_dataset"),
Expand Down Expand Up @@ -622,9 +621,9 @@ Sequence DDL is not implemented on this backend, so `schema_name` on it never
reaches a renderer:

```python
CreateSequenceExpression(d, "s_orders", schema_name="app").to_sql()
CreateSequenceStatement(d, "s_orders", schema_name="app").to_sql()
# AttributeError: BigQueryDialect has no formatting method
# 'format_create_sequence_statement' (required by CreateSequenceExpression).
# 'format_create_sequence_statement' (required by CreateSequenceStatement).
```

`ALTER TABLE ... ADD/DROP INDEX` is refused at the capability level
Expand All @@ -634,22 +633,22 @@ the table is qualified by its own `TableExpression`, and a bare string for that
table is refused at construction:

```python
CreateIndexExpression(
CreateIndexStatement(
d, "idx_orders_id", TableExpression(d, "orders", schema_name="app"), ["id"],
schema_name="app",
).to_sql()[0]
# CREATE INDEX `app`.`idx_orders_id` ON `app`.`orders` (`id`)

CreateIndexExpression(
CreateIndexStatement(
d, "idx_shared", TableExpression(d, "orders", schema_name="sales"), ["user_id"],
schema_name="app",
).to_sql()[0]
# CREATE INDEX `app`.`idx_shared` ON `sales`.`orders` (`user_id`)

CreateIndexExpression(d, "idx_orders_id", "orders", ["id"], schema_name="app")
CreateIndexStatement(d, "idx_orders_id", "orders", ["id"], schema_name="app")
# TypeError: table must be a TableExpression, got str

DropIndexExpression(d, "idx_orders_id", schema_name="app").to_sql()[0]
DropIndexStatement(d, "idx_orders_id", schema_name="app").to_sql()[0]
# DROP INDEX `app`.`idx_orders_id`
```

Expand Down Expand Up @@ -765,11 +764,12 @@ count:
**This library currently carries one of them.** Stated plainly:

> **The table, view, column and index expressions in this backend accept a
> `schema_name` and nothing above it. `TableExpression`, `Column` and
> `WildcardExpression` have no project field,
> `schema_name` and nothing above it. `TableExpression`, `Column` and the
> wildcard nodes have no project field,
> and no dialect hook adds one. A dataset is always rendered as exactly one
> quoted segment followed by one dot, so a dotted value stays inside that
> segment.** (`Column` and `WildcardExpression` discard the value; see
> segment.** (A wildcard's qualifier rides its `NamedRelationRef`, and `Column`
> checks then discards the value; see
> [Columns never carry the dataset](#columns-never-carry-the-dataset). On an index
> statement `schema_name` covers the index name only — the table reference has its
> own dataset.)
Expand Down Expand Up @@ -839,7 +839,7 @@ dialect.get_supported_introspection_scopes() # []
Every metadata query formatter therefore refuses:

```python
TableInfoExpression(d, "orders").to_sql()
TableInfoQueryStatement(d, "orders").to_sql()
# UnsupportedFeatureError: 'BigQuery' dialect does not support table
# introspection.
```
Expand Down
Loading
Loading