Skip to content

Commit fb06dd3

Browse files
robacourtclaude
andauthored
feat(sync-service): remove subquery feature flags (#4741)
## Summary Subqueries in shape WHERE clauses are now permanently enabled and treated as a generally-available feature. This removes the two gating flags — `allow_subqueries` and `tagged_subqueries` — so there is no way to turn subqueries off, and cleans up the code that only existed to support their disabled paths. ## Changes - **config**: empty `@known_feature_flags` and simplify the `feature_flags` default. The generic `ELECTRIC_FEATURE_FLAGS` mechanism (env var, parsing, plumbing) is intentionally **retained** for future flags — it just has no flags defined now. - **`allow_subqueries` removal**: - `shape.ex`: drop `check_feature_flag/2` — subqueries are always allowed. - `consumer/state.ex`: drop the `"allow_subqueries" in feature_flags` disjunct. Non-subquery shapes keep fragment streaming, matching current production behavior; only shapes with dependencies / inner subquery shapes use full-txn buffering. - **`tagged_subqueries` removal** — the dependency move policy collapses to always stream: - `event_handler_builder.ex`: remove `dependency_move_policy/2`. - `subqueries/shape_info.ex`: remove the `dependency_move_policy` field and type. - `event_handler/subqueries/steady.ex`: remove the `:invalidate_on_dependency_move` clause that returned `{:error, :unsupported_subquery}`. - `consumer.ex`: remove the now-unreachable `:unsupported_subquery` error handler. - **tests**: remove all `@tag allow_subqueries: false` tags + the setup conditional, strip `feature_flags: ["allow_subqueries"]` args, and delete the two obsolete invalidate-policy tests. - **docs**: reframe subqueries as GA — remove the "Preview feature" callout and flag wording in `shapes.md` (section/anchor renamed to `#subqueries`), collapse the `config.md` Feature Flags section to a minimal `ELECTRIC_FEATURE_FLAGS` entry, move subqueries out of "Unsupported" in the client skill, and drop the `ELECTRIC_FEATURE_FLAGS` line from the agents-server dev compose. ## Notes With the test-env `feature_flags` default now empty and the `allow_subqueries` disjunct gone, simple non-subquery shapes exercise fragment streaming by default in tests (matching production) rather than txn-buffering. The full suite passes under that. ## Test Plan - [x] `mix format --check-formatted` clean - [x] `mix compile --warnings-as-errors` clean (confirms no dead clauses / unused vars) - [x] Full suite: **2277 passed, 0 failures**, 25 excluded - [x] Subquery shape requests succeed without any `ELECTRIC_FEATURE_FLAGS` set (covered by `serve_shape_plug_test` / `subqueries_test`) --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/electric-sql/codesmith/electric/pr/4741"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787816001&installation_model_id=8736&pr_number=4741&repository=electric-sql%2Felectric&return_to=https%3A%2F%2Fgithub.com%2Felectric-sql%2Felectric%2Fpull%2F4741&signature=fd5a7f3eddfda2bf413c9b925e4089a9c54cc35327a5441fe86d84188b5145a3"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ac9fb39 commit fb06dd3

17 files changed

Lines changed: 46 additions & 170 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@core/sync-service": patch
3+
"@electric-sql/client": patch
4+
"@electric-ax/agents-server": patch
5+
---
6+
7+
Subqueries in shape WHERE clauses are now generally available and always enabled, including incremental move handling for compound `AND`/`OR`/`NOT` expressions. The `allow_subqueries` and `tagged_subqueries` feature flags have been removed — they no longer need to be set via `ELECTRIC_FEATURE_FLAGS`.

packages/agents-server/docker-compose.dev.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ services:
3030
environment:
3131
DATABASE_URL: postgresql://electric_agents:electric_agents@postgres:5432/electric_agents
3232
ELECTRIC_INSECURE: 'true'
33-
ELECTRIC_FEATURE_FLAGS: allow_subqueries
3433
depends_on:
3534
postgres:
3635
condition: service_healthy

packages/sync-service/lib/electric/config.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ defmodule Electric.Config do
3838

3939
@build_env Mix.env()
4040

41-
@known_feature_flags ~w[allow_subqueries tagged_subqueries]
41+
@known_feature_flags ~w[]
4242
@default_storage_dir "./persistent"
4343

4444
@defaults [
@@ -133,7 +133,7 @@ defmodule Electric.Config do
133133
consumer_gc_heap_threshold: nil,
134134
## Misc
135135
process_registry_partitions: &Electric.Config.Defaults.process_registry_partitions/0,
136-
feature_flags: if(Mix.env() == :test, do: @known_feature_flags, else: []),
136+
feature_flags: [],
137137
publication_refresh_period: 60_000,
138138
schema_reconciler_period: 60_000,
139139
snapshot_timeout_to_first_data: :timer.seconds(30),

packages/sync-service/lib/electric/shapes/consumer.ex

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,10 +1026,6 @@ defmodule Electric.Shapes.Consumer do
10261026
handle_txn_with_truncate(xid, state)
10271027
end
10281028

1029-
defp handle_event_error(state, :unsupported_subquery) do
1030-
mark_for_removal(state)
1031-
end
1032-
10331029
defp handle_event_error(state, :buffer_overflow) do
10341030
Logger.warning("Subquery buffer overflow for #{state.shape_handle} - terminating shape")
10351031

packages/sync-service/lib/electric/shapes/consumer/event_handler/subqueries/steady.ex

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,6 @@ defmodule Electric.Shapes.Consumer.EventHandler.Subqueries.Steady do
3636
{:ok, state, []}
3737
end
3838

39-
def handle_event(
40-
%__MODULE__{
41-
shape_info: %ShapeInfo{dependency_move_policy: :invalidate_on_dependency_move}
42-
},
43-
{:materializer_changes, _dep_handle, _payload}
44-
) do
45-
{:error, :unsupported_subquery}
46-
end
47-
4839
def handle_event(%__MODULE__{} = state, {:materializer_changes, dep_handle, payload}) do
4940
subquery_ref = RefResolver.ref_from_dep_handle!(state.shape_info.ref_resolver, dep_handle)
5041
dep_index = subquery_ref |> List.last() |> String.to_integer()

packages/sync-service/lib/electric/shapes/consumer/event_handler_builder.ex

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ defmodule Electric.Shapes.Consumer.EventHandlerBuilder do
1313
def build(%State{shape: %Shape{shape_dependencies_handles: dep_handles}} = state, action)
1414
when dep_handles != [] do
1515
{:ok, dnf_plan} = DnfPlan.compile(state.shape)
16-
dependency_move_policy = dependency_move_policy(state.stack_id, state.shape)
1716

1817
{views, dep_handle_to_ref, dep_index_to_ref} =
1918
dep_handles
@@ -44,8 +43,7 @@ defmodule Electric.Shapes.Consumer.EventHandlerBuilder do
4443
dnf_plan: dnf_plan,
4544
ref_resolver:
4645
Electric.Shapes.Consumer.Subqueries.RefResolver.new(dep_handle_to_ref, dep_index_to_ref),
47-
buffer_max_transactions: buffer_max_transactions,
48-
dependency_move_policy: dependency_move_policy
46+
buffer_max_transactions: buffer_max_transactions
4947
},
5048
views: views
5149
}
@@ -63,14 +61,4 @@ defmodule Electric.Shapes.Consumer.EventHandlerBuilder do
6361

6462
{:ok, handler, [%SetupEffects.SubscribeShape{action: action}]}
6563
end
66-
67-
defp dependency_move_policy(stack_id, _shape) do
68-
feature_flags = Electric.StackConfig.lookup(stack_id, :feature_flags, [])
69-
70-
if "tagged_subqueries" not in feature_flags do
71-
:invalidate_on_dependency_move
72-
else
73-
:stream_dependency_moves
74-
end
75-
end
7664
end

packages/sync-service/lib/electric/shapes/consumer/state.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ defmodule Electric.Shapes.Consumer.State do
119119

120120
@spec initialize_shape(uninitialized_t(), Shape.t(), map()) :: uninitialized_t()
121121
def initialize_shape(%__MODULE__{} = state, shape, opts) do
122-
feature_flags = Map.get(opts, :feature_flags, [])
123122
is_subquery_shape? = Map.get(opts, :is_subquery_shape?, false)
124123

125124
%{
@@ -128,8 +127,7 @@ defmodule Electric.Shapes.Consumer.State do
128127
# Enable direct fragment-to-storage streaming for shapes without subquery dependencies
129128
# and if the current shape itself isn't an inner shape of a shape with subqueries.
130129
write_unit:
131-
if "allow_subqueries" in feature_flags or shape.shape_dependencies != [] or
132-
is_subquery_shape? do
130+
if shape.shape_dependencies != [] or is_subquery_shape? do
133131
@write_unit_txn
134132
else
135133
@write_unit_txn_fragment

packages/sync-service/lib/electric/shapes/consumer/subqueries/shape_info.ex

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,21 @@ defmodule Electric.Shapes.Consumer.Subqueries.ShapeInfo do
55
alias Electric.Shapes.DnfPlan
66
alias Electric.Shapes.Shape
77

8-
@type dependency_move_policy :: :stream_dependency_moves | :invalidate_on_dependency_move
9-
108
@enforce_keys [
119
:shape,
1210
:stack_id,
1311
:shape_handle,
1412
:dnf_plan,
1513
:ref_resolver,
16-
:buffer_max_transactions,
17-
:dependency_move_policy
14+
:buffer_max_transactions
1815
]
1916
defstruct [
2017
:shape,
2118
:stack_id,
2219
:shape_handle,
2320
:dnf_plan,
2421
:ref_resolver,
25-
:buffer_max_transactions,
26-
:dependency_move_policy
22+
:buffer_max_transactions
2723
]
2824

2925
@type t() :: %__MODULE__{
@@ -32,7 +28,6 @@ defmodule Electric.Shapes.Consumer.Subqueries.ShapeInfo do
3228
shape_handle: String.t(),
3329
dnf_plan: DnfPlan.t(),
3430
ref_resolver: RefResolver.t(),
35-
buffer_max_transactions: pos_integer(),
36-
dependency_move_policy: dependency_move_policy()
31+
buffer_max_transactions: pos_integer()
3732
}
3833
end

packages/sync-service/lib/electric/shapes/shape.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@ defmodule Electric.Shapes.Shape do
295295
defp validate_where_clause(where, %{inspector: inspector} = opts, refs) do
296296
with {:ok, where} <- Parser.parse_query(where),
297297
{:ok, subqueries} <- Parser.extract_subqueries(where),
298-
:ok <- check_feature_flag(subqueries, opts),
299298
{:ok, shape_dependencies, sublink_dependency_indexes} <-
300299
build_shape_dependencies(subqueries, opts),
301300
{:ok, dependency_refs} <- build_dependency_refs(shape_dependencies, inspector),
@@ -318,15 +317,6 @@ defmodule Electric.Shapes.Shape do
318317
end
319318
end
320319

321-
defp check_feature_flag(subqueries, opts) do
322-
if subqueries != [] and
323-
not Enum.member?(opts.feature_flags, "allow_subqueries") do
324-
{:error, {:where, "Subqueries are not supported"}}
325-
else
326-
:ok
327-
end
328-
end
329-
330320
defp make_opts_from_select(select, opts) do
331321
with {:ok, {columns, from, where}} <- Parser.extract_parts_from_select(select) do
332322
{:ok,

packages/sync-service/test/electric/plug/serve_shape_plug_test.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,6 @@ defmodule Electric.Plug.ServeShapePlugTest do
267267
ctx =
268268
ctx
269269
|> Map.put(:inspector, @subquery_inspector)
270-
|> Map.put(:feature_flags, ["allow_subqueries"])
271270

272271
Repatch.patch(Electric.Shapes, :fetch_handle_by_shape, fn _, _ ->
273272
flunk("should reject before checking whether the shape already exists")

0 commit comments

Comments
 (0)