diff --git a/.changeset/subqueries-generally-available.md b/.changeset/subqueries-generally-available.md new file mode 100644 index 0000000000..73188ca61d --- /dev/null +++ b/.changeset/subqueries-generally-available.md @@ -0,0 +1,7 @@ +--- +"@core/sync-service": patch +"@electric-sql/client": patch +"@electric-ax/agents-server": patch +--- + +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`. diff --git a/packages/agents-server/docker-compose.dev.yml b/packages/agents-server/docker-compose.dev.yml index e69adb59c0..7de9a84c07 100644 --- a/packages/agents-server/docker-compose.dev.yml +++ b/packages/agents-server/docker-compose.dev.yml @@ -30,7 +30,6 @@ services: environment: DATABASE_URL: postgresql://electric_agents:electric_agents@postgres:5432/electric_agents ELECTRIC_INSECURE: 'true' - ELECTRIC_FEATURE_FLAGS: allow_subqueries depends_on: postgres: condition: service_healthy diff --git a/packages/sync-service/lib/electric/config.ex b/packages/sync-service/lib/electric/config.ex index 31a0530d2a..dfff68511d 100644 --- a/packages/sync-service/lib/electric/config.ex +++ b/packages/sync-service/lib/electric/config.ex @@ -38,7 +38,7 @@ defmodule Electric.Config do @build_env Mix.env() - @known_feature_flags ~w[allow_subqueries tagged_subqueries] + @known_feature_flags ~w[] @default_storage_dir "./persistent" @defaults [ @@ -133,7 +133,7 @@ defmodule Electric.Config do consumer_gc_heap_threshold: nil, ## Misc process_registry_partitions: &Electric.Config.Defaults.process_registry_partitions/0, - feature_flags: if(Mix.env() == :test, do: @known_feature_flags, else: []), + feature_flags: [], publication_refresh_period: 60_000, schema_reconciler_period: 60_000, snapshot_timeout_to_first_data: :timer.seconds(30), diff --git a/packages/sync-service/lib/electric/shapes/consumer.ex b/packages/sync-service/lib/electric/shapes/consumer.ex index b12ea4d6c5..c43ccecc04 100644 --- a/packages/sync-service/lib/electric/shapes/consumer.ex +++ b/packages/sync-service/lib/electric/shapes/consumer.ex @@ -1026,10 +1026,6 @@ defmodule Electric.Shapes.Consumer do handle_txn_with_truncate(xid, state) end - defp handle_event_error(state, :unsupported_subquery) do - mark_for_removal(state) - end - defp handle_event_error(state, :buffer_overflow) do Logger.warning("Subquery buffer overflow for #{state.shape_handle} - terminating shape") diff --git a/packages/sync-service/lib/electric/shapes/consumer/event_handler/subqueries/steady.ex b/packages/sync-service/lib/electric/shapes/consumer/event_handler/subqueries/steady.ex index 053ba678e5..f282fb64be 100644 --- a/packages/sync-service/lib/electric/shapes/consumer/event_handler/subqueries/steady.ex +++ b/packages/sync-service/lib/electric/shapes/consumer/event_handler/subqueries/steady.ex @@ -36,15 +36,6 @@ defmodule Electric.Shapes.Consumer.EventHandler.Subqueries.Steady do {:ok, state, []} end - def handle_event( - %__MODULE__{ - shape_info: %ShapeInfo{dependency_move_policy: :invalidate_on_dependency_move} - }, - {:materializer_changes, _dep_handle, _payload} - ) do - {:error, :unsupported_subquery} - end - def handle_event(%__MODULE__{} = state, {:materializer_changes, dep_handle, payload}) do subquery_ref = RefResolver.ref_from_dep_handle!(state.shape_info.ref_resolver, dep_handle) dep_index = subquery_ref |> List.last() |> String.to_integer() diff --git a/packages/sync-service/lib/electric/shapes/consumer/event_handler_builder.ex b/packages/sync-service/lib/electric/shapes/consumer/event_handler_builder.ex index feb251e914..f2087889b5 100644 --- a/packages/sync-service/lib/electric/shapes/consumer/event_handler_builder.ex +++ b/packages/sync-service/lib/electric/shapes/consumer/event_handler_builder.ex @@ -13,7 +13,6 @@ defmodule Electric.Shapes.Consumer.EventHandlerBuilder do def build(%State{shape: %Shape{shape_dependencies_handles: dep_handles}} = state, action) when dep_handles != [] do {:ok, dnf_plan} = DnfPlan.compile(state.shape) - dependency_move_policy = dependency_move_policy(state.stack_id, state.shape) {views, dep_handle_to_ref, dep_index_to_ref} = dep_handles @@ -44,8 +43,7 @@ defmodule Electric.Shapes.Consumer.EventHandlerBuilder do dnf_plan: dnf_plan, ref_resolver: Electric.Shapes.Consumer.Subqueries.RefResolver.new(dep_handle_to_ref, dep_index_to_ref), - buffer_max_transactions: buffer_max_transactions, - dependency_move_policy: dependency_move_policy + buffer_max_transactions: buffer_max_transactions }, views: views } @@ -63,14 +61,4 @@ defmodule Electric.Shapes.Consumer.EventHandlerBuilder do {:ok, handler, [%SetupEffects.SubscribeShape{action: action}]} end - - defp dependency_move_policy(stack_id, _shape) do - feature_flags = Electric.StackConfig.lookup(stack_id, :feature_flags, []) - - if "tagged_subqueries" not in feature_flags do - :invalidate_on_dependency_move - else - :stream_dependency_moves - end - end end diff --git a/packages/sync-service/lib/electric/shapes/consumer/state.ex b/packages/sync-service/lib/electric/shapes/consumer/state.ex index e23edee4dc..d87cad5cc9 100644 --- a/packages/sync-service/lib/electric/shapes/consumer/state.ex +++ b/packages/sync-service/lib/electric/shapes/consumer/state.ex @@ -119,7 +119,6 @@ defmodule Electric.Shapes.Consumer.State do @spec initialize_shape(uninitialized_t(), Shape.t(), map()) :: uninitialized_t() def initialize_shape(%__MODULE__{} = state, shape, opts) do - feature_flags = Map.get(opts, :feature_flags, []) is_subquery_shape? = Map.get(opts, :is_subquery_shape?, false) %{ @@ -128,8 +127,7 @@ defmodule Electric.Shapes.Consumer.State do # Enable direct fragment-to-storage streaming for shapes without subquery dependencies # and if the current shape itself isn't an inner shape of a shape with subqueries. write_unit: - if "allow_subqueries" in feature_flags or shape.shape_dependencies != [] or - is_subquery_shape? do + if shape.shape_dependencies != [] or is_subquery_shape? do @write_unit_txn else @write_unit_txn_fragment diff --git a/packages/sync-service/lib/electric/shapes/consumer/subqueries/shape_info.ex b/packages/sync-service/lib/electric/shapes/consumer/subqueries/shape_info.ex index 5f414b09c0..1458d0b86c 100644 --- a/packages/sync-service/lib/electric/shapes/consumer/subqueries/shape_info.ex +++ b/packages/sync-service/lib/electric/shapes/consumer/subqueries/shape_info.ex @@ -5,16 +5,13 @@ defmodule Electric.Shapes.Consumer.Subqueries.ShapeInfo do alias Electric.Shapes.DnfPlan alias Electric.Shapes.Shape - @type dependency_move_policy :: :stream_dependency_moves | :invalidate_on_dependency_move - @enforce_keys [ :shape, :stack_id, :shape_handle, :dnf_plan, :ref_resolver, - :buffer_max_transactions, - :dependency_move_policy + :buffer_max_transactions ] defstruct [ :shape, @@ -22,8 +19,7 @@ defmodule Electric.Shapes.Consumer.Subqueries.ShapeInfo do :shape_handle, :dnf_plan, :ref_resolver, - :buffer_max_transactions, - :dependency_move_policy + :buffer_max_transactions ] @type t() :: %__MODULE__{ @@ -32,7 +28,6 @@ defmodule Electric.Shapes.Consumer.Subqueries.ShapeInfo do shape_handle: String.t(), dnf_plan: DnfPlan.t(), ref_resolver: RefResolver.t(), - buffer_max_transactions: pos_integer(), - dependency_move_policy: dependency_move_policy() + buffer_max_transactions: pos_integer() } end diff --git a/packages/sync-service/lib/electric/shapes/shape.ex b/packages/sync-service/lib/electric/shapes/shape.ex index 1683366f14..1c4d298607 100644 --- a/packages/sync-service/lib/electric/shapes/shape.ex +++ b/packages/sync-service/lib/electric/shapes/shape.ex @@ -295,7 +295,6 @@ defmodule Electric.Shapes.Shape do defp validate_where_clause(where, %{inspector: inspector} = opts, refs) do with {:ok, where} <- Parser.parse_query(where), {:ok, subqueries} <- Parser.extract_subqueries(where), - :ok <- check_feature_flag(subqueries, opts), {:ok, shape_dependencies, sublink_dependency_indexes} <- build_shape_dependencies(subqueries, opts), {:ok, dependency_refs} <- build_dependency_refs(shape_dependencies, inspector), @@ -318,15 +317,6 @@ defmodule Electric.Shapes.Shape do end end - defp check_feature_flag(subqueries, opts) do - if subqueries != [] and - not Enum.member?(opts.feature_flags, "allow_subqueries") do - {:error, {:where, "Subqueries are not supported"}} - else - :ok - end - end - defp make_opts_from_select(select, opts) do with {:ok, {columns, from, where}} <- Parser.extract_parts_from_select(select) do {:ok, diff --git a/packages/sync-service/test/electric/plug/serve_shape_plug_test.exs b/packages/sync-service/test/electric/plug/serve_shape_plug_test.exs index 2a66b6c72f..8753c0eb42 100644 --- a/packages/sync-service/test/electric/plug/serve_shape_plug_test.exs +++ b/packages/sync-service/test/electric/plug/serve_shape_plug_test.exs @@ -267,7 +267,6 @@ defmodule Electric.Plug.ServeShapePlugTest do ctx = ctx |> Map.put(:inspector, @subquery_inspector) - |> Map.put(:feature_flags, ["allow_subqueries"]) Repatch.patch(Electric.Shapes, :fetch_handle_by_shape, fn _, _ -> flunk("should reject before checking whether the shape already exists") diff --git a/packages/sync-service/test/electric/shapes/consumer/event_handler/subqueries_test.exs b/packages/sync-service/test/electric/shapes/consumer/event_handler/subqueries_test.exs index 0978b3ce3e..e24002a7b9 100644 --- a/packages/sync-service/test/electric/shapes/consumer/event_handler/subqueries_test.exs +++ b/packages/sync-service/test/electric/shapes/consumer/event_handler/subqueries_test.exs @@ -42,38 +42,6 @@ defmodule Electric.Shapes.Consumer.EventHandler.SubqueriesTest do ] = plan end - test "still converts root transactions when dependency moves are configured to invalidate" do - handler = - new_handler( - subquery_view: MapSet.new([1]), - dependency_move_policy: :invalidate_on_dependency_move - ) - - assert {:ok, %Steady{}, plan} = - EventHandler.handle_event( - handler, - txn(50, [child_insert("1", "1"), child_insert("2", "2")]) - ) - - assert [ - %Effects.AppendChanges{ - changes: [%Changes.NewRecord{record: %{"id" => "1"}, last?: true}] - }, - %Effects.NotifyFlushed{log_offset: _} - ] = plan - end - - test "returns unsupported_subquery when dependency moves are configured to invalidate" do - handler = new_handler(dependency_move_policy: :invalidate_on_dependency_move) - dep_handle = dep_handle(handler) - - assert {:error, :unsupported_subquery} = - EventHandler.handle_event( - handler, - {:materializer_changes, dep_handle, %{move_in: [{1, "1"}], move_out: []}} - ) - end - test "negated subquery turns dependency move-in into an outer move-out" do handler = new_handler(shape: negated_shape()) dep_handle = dep_handle(handler) @@ -876,9 +844,7 @@ defmodule Electric.Shapes.Consumer.EventHandler.SubqueriesTest do dnf_plan: dnf_plan, ref_resolver: RefResolver.new(%{dep_handle => {0, ["$sublink", "0"]}}, %{0 => ["$sublink", "0"]}), - buffer_max_transactions: Keyword.get(opts, :buffer_max_transactions, 1000), - dependency_move_policy: - Keyword.get(opts, :dependency_move_policy, :stream_dependency_moves) + buffer_max_transactions: Keyword.get(opts, :buffer_max_transactions, 1000) }, views: %{["$sublink", "0"] => Keyword.get(opts, :subquery_view, MapSet.new())} } @@ -895,8 +861,7 @@ defmodule Electric.Shapes.Consumer.EventHandler.SubqueriesTest do defp shape do Shape.new!("child", where: "parent_id IN (SELECT id FROM public.parent WHERE value = 'keep')", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) |> fill_handles() end @@ -904,8 +869,7 @@ defmodule Electric.Shapes.Consumer.EventHandler.SubqueriesTest do defp negated_shape do Shape.new!("child", where: "parent_id NOT IN (SELECT id FROM public.parent WHERE value = 'keep')", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) |> fill_handles() end diff --git a/packages/sync-service/test/electric/shapes/consumer_test.exs b/packages/sync-service/test/electric/shapes/consumer_test.exs index 717969502a..cdbbb1439a 100644 --- a/packages/sync-service/test/electric/shapes/consumer_test.exs +++ b/packages/sync-service/test/electric/shapes/consumer_test.exs @@ -647,10 +647,6 @@ defmodule Electric.Shapes.ConsumerTest do Map.get(ctx, :shape_suspend_after, 60_000) ) - if not Map.get(ctx, :allow_subqueries, true) do - Electric.StackConfig.put(ctx.stack_id, :feature_flags, []) - end - :ok end @@ -781,7 +777,6 @@ defmodule Electric.Shapes.ConsumerTest do get_log_items_from_storage(LogOffset.last_before_real_offsets(), shape_storage) end - @tag allow_subqueries: false test "duplicate txn fragment handling is idempotent", ctx do {shape_handle, _} = ShapeCache.get_or_create_shape_handle(@shape1, ctx.stack_id) :started = ShapeCache.await_snapshot_start(shape_handle, ctx.stack_id) @@ -890,7 +885,6 @@ defmodule Electric.Shapes.ConsumerTest do refute_receive {^ref, :new_changes, _} end - @tag allow_subqueries: false test "skips an already-applied multi-fragment transaction replayed past a fresh log collector", ctx do # Multi-fragment variant of "skips an already-applied transaction replayed @@ -1106,8 +1100,7 @@ defmodule Electric.Shapes.ConsumerTest do assert_receive {:flush_boundary_updated, ^tx_offset} end - @tag allow_subqueries: false, - delay_snapshot_creation?: true, + @tag delay_snapshot_creation?: true, with_pure_file_storage_opts: [flush_period: 1] test "transaction fragments are buffered until snapshot xmin is known", ctx do register_as_replication_client(ctx.stack_id) @@ -1319,8 +1312,7 @@ defmodule Electric.Shapes.ConsumerTest do assert {:ok, last_log_offset} == Storage.fetch_latest_offset(shape_storage) end - @tag allow_subqueries: false, - pg_snapshot: {10, 13, [10]}, + @tag pg_snapshot: {10, 13, [10]}, with_pure_file_storage_opts: [flush_period: 1] test "fragments that belong to transactions already included in the snapshot are skipped", ctx do @@ -1966,7 +1958,7 @@ defmodule Electric.Shapes.ConsumerTest do assert [] == :ets.tab2list(table) end - @tag allow_subqueries: false, with_pure_file_storage_opts: [flush_period: 1] + @tag with_pure_file_storage_opts: [flush_period: 1] test "writes txn fragments to storage immediately but keeps txn boundaries when flushing", ctx do {shape_handle, _} = ShapeCache.get_or_create_shape_handle(@shape1, ctx.stack_id) @@ -2095,7 +2087,7 @@ defmodule Electric.Shapes.ConsumerTest do assert_receive {:flush_boundary_updated, ^offset} end - @tag allow_subqueries: false, with_pure_file_storage_opts: [flush_period: 1] + @tag with_pure_file_storage_opts: [flush_period: 1] test "flush notification for multi-fragment txn is not lost when storage flushes before commit fragment", %{stack_id: stack_id} = ctx do # Regression test for https://github.com/electric-sql/electric/issues/3985 @@ -2195,7 +2187,7 @@ defmodule Electric.Shapes.ConsumerTest do assert_receive {:flush_boundary_updated, ^tx_offset}, @receive_timeout end - @tag allow_subqueries: false, with_pure_file_storage_opts: [flush_period: 10_000] + @tag with_pure_file_storage_opts: [flush_period: 10_000] test "flush notification offset is aligned when storage flushes before commit arrives at consumer", %{stack_id: stack_id} do # Regression test for https://github.com/electric-sql/electric/issues/4063 @@ -2299,7 +2291,7 @@ defmodule Electric.Shapes.ConsumerTest do assert_receive {:flush_boundary_updated, ^tx_offset}, @receive_timeout end - @tag allow_subqueries: false, with_pure_file_storage_opts: [flush_period: 1] + @tag with_pure_file_storage_opts: [flush_period: 1] test "dead consumer doesn't block flush notifications from advancing as live consumers flush to storage", ctx do {shape_handle1, _} = ShapeCache.get_or_create_shape_handle(@shape1, ctx.stack_id) diff --git a/packages/sync-service/test/electric/shapes/filter_test.exs b/packages/sync-service/test/electric/shapes/filter_test.exs index bc99e1acb6..1791e625ab 100644 --- a/packages/sync-service/test/electric/shapes/filter_test.exs +++ b/packages/sync-service/test/electric/shapes/filter_test.exs @@ -55,8 +55,7 @@ defmodule Electric.Shapes.FilterTest do shape = Shape.new!("table", where: where, - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) assert Filter.indexed_shape?(shape), "#{where} should be indexed" @@ -617,23 +616,19 @@ defmodule Electric.Shapes.FilterTest do Shape.new!("table", where: "id IN (1, 2) AND number > 5", inspector: @inspector), Shape.new!("table", where: "id IN (SELECT id FROM another_table) OR id = 1", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ), Shape.new!("table", where: "id IN (SELECT id FROM another_table) OR number > 5", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ), Shape.new!("table", where: "id IN (SELECT id FROM another_table)", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ), Shape.new!("table", where: "NOT id IN (SELECT id FROM another_table)", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) ] @@ -673,8 +668,7 @@ defmodule Electric.Shapes.FilterTest do shape = Shape.new!("table", where: "id IN (SELECT id FROM another_table)", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) Filter.add_shape(filter, id, shape) @@ -695,8 +689,7 @@ defmodule Electric.Shapes.FilterTest do shape = Shape.new!("table", where: "id IN (SELECT id FROM another_table)", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) Filter.add_shape(filter, shape_id, shape) @@ -1011,8 +1004,7 @@ defmodule Electric.Shapes.FilterTest do shape = Shape.new!("table", where: "number = #{i} AND id IN (SELECT id FROM another_table)", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) Filter.add_shape(filter, i, shape) @@ -1036,8 +1028,7 @@ defmodule Electric.Shapes.FilterTest do shape = Shape.new!("table", where: "id IN (SELECT id FROM another_table)", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) Filter.add_shape(filter, i, shape) @@ -1819,8 +1810,7 @@ defmodule Electric.Shapes.FilterTest do sample_shape = Shape.new!("table", where: "id IN (SELECT id FROM another_table)", - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) {_hash, real_handle} = Shape.generate_id(sample_shape) @@ -1878,8 +1868,7 @@ defmodule Electric.Shapes.FilterTest do shape = Shape.new!("table", where: where_fun.(i), - inspector: @inspector, - feature_flags: ["allow_subqueries"] + inspector: @inspector ) handle = realistic_handle(shape, i) diff --git a/packages/sync-service/test/electric/shapes/shape_test.exs b/packages/sync-service/test/electric/shapes/shape_test.exs index baac00ee08..55f66c5f3c 100644 --- a/packages/sync-service/test/electric/shapes/shape_test.exs +++ b/packages/sync-service/test/electric/shapes/shape_test.exs @@ -1000,7 +1000,6 @@ defmodule Electric.Shapes.ShapeTest do assert {:ok, %Shape{where: where, shape_dependencies: dependencies}} = Shape.new("item", inspector: inspector, - feature_flags: ["allow_subqueries"], where: "id IN (SELECT a FROM rel WHERE kind = 'k') OR id IN (SELECT b FROM rel WHERE kind = 'k')" ) diff --git a/packages/typescript-client/skills/electric-shapes/references/where-clause.md b/packages/typescript-client/skills/electric-shapes/references/where-clause.md index ecf6256877..6c8098ed36 100644 --- a/packages/typescript-client/skills/electric-shapes/references/where-clause.md +++ b/packages/typescript-client/skills/electric-shapes/references/where-clause.md @@ -21,7 +21,10 @@ - `timetz` — not supported in WHERE - Non-deterministic functions: `now()`, `random()`, `count()`, `current_timestamp` - Aggregate functions -- Subqueries (experimental, requires `ELECTRIC_FEATURE_FLAGS=allow_subqueries`) + +## Subqueries + +Supported in WHERE clauses to filter rows based on data in other tables, e.g. `id IN (SELECT user_id FROM memberships WHERE org_id = $1)`. ## Positional Parameters diff --git a/website/docs/sync/api/config.md b/website/docs/sync/api/config.md index 5add4bd4a1..8bb9d669d7 100644 --- a/website/docs/sync/api/config.md +++ b/website/docs/sync/api/config.md @@ -465,48 +465,18 @@ Consumer processes are partitioned across some number of supervisors to improve ## Feature Flags -Feature flags enable advanced features and staged rollouts for capabilities that are not yet enabled by default in production. +Feature flags enable staged rollouts of new capabilities that are not yet enabled by default in production. ### ELECTRIC_FEATURE_FLAGS + defaultValue=""> -**Available flags:** - -- `allow_subqueries` - Enables preview subquery support in shape WHERE clauses -- `tagged_subqueries` - Enables preview incremental subquery move handling, including compound boolean expressions with compatible clients +Comma-separated list of feature flags to enable. There are currently no feature flags available. -:::warning Client compatibility -Electric 1.6's incremental handling for compound subquery expressions changes the client protocol. Upgrade clients before enabling the server rollout. TanStack DB clients need `@tanstack/db >= 0.6.2` and `@tanstack/electric-db-collection >= 0.3.0`. -::: - -### allow_subqueries - -Enables support for subqueries in the WHERE clause of [shape](/docs/sync/guides/shapes) definitions. When enabled, you can use queries in the form: - -```sql -WHERE id IN (SELECT user_id FROM memberships WHERE org_id = 'org_123') -``` - -This allows creating shapes that filter based on related data in other tables, enabling more complex data synchronization patterns. - -**Status:** Preview. Disabled by default in production until enabled with `ELECTRIC_FEATURE_FLAGS`. - -### tagged_subqueries - -Subqueries create dependency trees between shapes. This flag enables incremental move handling when dependency rows change, including compound `WHERE` expressions that combine subqueries with `AND`, `OR`, and `NOT`. - -Before Electric 1.6, complex boolean combinations around subqueries could still invalidate the shape and return a `409` on a move. With this flag enabled and compatible clients, those changes are reconciled in-stream instead. - -See [discussion #2931](https://github.com/electric-sql/electric/discussions/2931) for more details about this feature. - -**Status:** Preview rollout flag for subquery move handling. Disabled by default in production. Requires `allow_subqueries` to be enabled. - ## Caching ### ELECTRIC_CACHE_MAX_AGE diff --git a/website/docs/sync/guides/shapes.md b/website/docs/sync/guides/shapes.md index 6ecc2a0653..9554ec2a64 100644 --- a/website/docs/sync/guides/shapes.md +++ b/website/docs/sync/guides/shapes.md @@ -52,7 +52,7 @@ Shapes are defined by: A shape contains all of the rows in the table that match the where clause, if provided. If a columns clause is provided, the synced rows will only contain those selected columns. > [!Warning] Limitations -> Shapes are currently [single table](#single-table), though you can use [subqueries](#subqueries-preview) to filter based on related data. Shape definitions are [immutable](#immutable). +> Shapes are currently [single table](#single-table), though you can use [subqueries](#subqueries) to filter based on related data. Shape definitions are [immutable](#immutable). > [!Warning] Security > Production apps should request shapes through your backend API for authorization and security. See the [auth guide](/docs/sync/guides/auth). @@ -121,9 +121,9 @@ Where clauses have the following constraints: 1. can't use non-deterministic SQL functions like `count()` or `now()` -#### Subqueries (preview) +#### Subqueries -Electric has preview support for subqueries in where clauses, allowing you to filter rows based on data in other tables. This enables relational filtering patterns such as memberships, sharing rules, parent-child traversal, and exclusions while the feature remains gated behind flags. +Electric supports subqueries in where clauses, allowing you to filter rows based on data in other tables. This enables relational filtering patterns such as memberships, sharing rules, parent-child traversal, and exclusions. For example, you can sync only users who belong to a specific organization: @@ -189,11 +189,7 @@ const shape = new Shape(stream) When a shape uses a subquery, Electric tracks the dependency between tables. If the data in the subquery changes (e.g., a project becomes archived), rows will automatically move in or out of the shape without the row itself being modified. -Electric 1.6 keeps these moves incremental even for compound expressions that use `AND`, `OR`, and `NOT` around subqueries. In older releases those cases could return `409` and force a full resync of the shape. - -:::info Preview feature -Subqueries are currently in preview and are enabled using `ELECTRIC_FEATURE_FLAGS=allow_subqueries,tagged_subqueries`. The flags gate availability rather than a separate syntax or API. See the [configuration docs](/docs/sync/api/config#allow_subqueries) for details. -::: +Electric keeps these moves incremental even for compound expressions that use `AND`, `OR`, and `NOT` around subqueries. In older releases those cases could return `409` and force a full resync of the shape. When constructing a where clause with user input as a filter, it's recommended to use a positional placeholder (`$1`) to avoid SQL injection-like situations. For example, if filtering a table on a user id, it's better to use `where=user = $1` with @@ -514,7 +510,7 @@ We currently optimize the evaluation of the following clauses: ### Single table -Shapes sync data from a single table. While you can use [subqueries](#subqueries-preview) to filter rows based on data in other tables, the shape only contains rows from the root table—not the related data itself. +Shapes sync data from a single table. While you can use [subqueries](#subqueries) to filter rows based on data in other tables, the shape only contains rows from the root table—not the related data itself. For syncing related data across tables, you currently need to use multiple shapes. In the [old version of Electric](https://legacy.electric-sql.com/docs/usage/data-access/shapes), Shapes had an include tree that allowed you to sync nested relations. The new Electric has not yet implemented support for include trees.