From c9c12da2ba1e610e0e526931e4df2b4b62551b56 Mon Sep 17 00:00:00 2001 From: Hiemanshu Sharma Date: Mon, 25 Nov 2024 16:11:13 +0530 Subject: [PATCH 1/4] Add support for security barrier and invoker --- lib/scenic/adapters/postgres.rb | 34 +++++++++++++++++++++------ lib/scenic/adapters/postgres/views.rb | 24 +++++++++++++++++-- lib/scenic/statements.rb | 33 ++++++++++++++++++++------ lib/scenic/view.rb | 14 ++++++++--- spec/scenic/statements_spec.rb | 16 ++++++------- 5 files changed, 94 insertions(+), 27 deletions(-) diff --git a/lib/scenic/adapters/postgres.rb b/lib/scenic/adapters/postgres.rb index b9cd13e3..4bca794f 100644 --- a/lib/scenic/adapters/postgres.rb +++ b/lib/scenic/adapters/postgres.rb @@ -59,10 +59,13 @@ def views # # @param name The name of the view to create # @param sql_definition The SQL schema for the view. + # @param security_barrier If we should enable security_barrier + # @param security_invoker If we should enable security_invoker # # @return [void] - def create_view(name, sql_definition) - execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};" + def create_view(name, sql_definition, security_barrier, security_invoker) + with_statement = build_with_statement(security_barrier, security_invoker) + execute "CREATE VIEW #{quote_table_name(name)} #{with_statement} AS #{sql_definition};" end # Updates a view in the database. @@ -79,11 +82,13 @@ def create_view(name, sql_definition) # # @param name The name of the view to update # @param sql_definition The SQL schema for the updated view. + # @param security_barrier If we should enable security_barrier + # @param security_invoker If we should enable security_invoker # # @return [void] - def update_view(name, sql_definition) + def update_view(name, sql_definition, security_barrier, security_invoker) drop_view(name) - create_view(name, sql_definition) + create_view(name, sql_definition, security_barrier, security_invoker) end # Replaces a view in the database using `CREATE OR REPLACE VIEW`. @@ -105,10 +110,13 @@ def update_view(name, sql_definition) # # @param name The name of the view to update # @param sql_definition The SQL schema for the updated view. + # @param security_barrier If we should enable security_barrier + # @param security_invoker If we should enable security_invoker # # @return [void] - def replace_view(name, sql_definition) - execute "CREATE OR REPLACE VIEW #{quote_table_name(name)} AS #{sql_definition};" + def replace_view(name, sql_definition, security_barrier, security_invoker) + with_statement = build_with_statement(security_barrier, security_invoker) + execute "CREATE OR REPLACE VIEW #{quote_table_name(name)} #{with_statement} AS #{sql_definition};" end # Drops the named view from the database @@ -201,7 +209,7 @@ def drop_materialized_view(name) # This is typically called from application code via {Scenic.database}. # # @param name The name of the materialized view to refresh. - # @param concurrently [Boolean] Whether the refreshs hould happen + # @param concurrently [Boolean] Whether the refresh should happen # concurrently or not. A concurrent refresh allows the view to be # refreshed without locking the view for select but requires that the # table have at least one unique index that covers all rows. Attempts to @@ -299,6 +307,18 @@ def refresh_dependencies_for(name, concurrently: false) concurrently: concurrently ) end + + def build_with_statement(security_barrier, security_invoker) + if security_invoker && security_barrier + return "WITH (security_barrier, security_invoker = true)" + elsif security_invoker + return "WITH (security_invoker = true)" + elsif security_barrier + return "WITH (security_barrier)" + end + + return "" + end end end end diff --git a/lib/scenic/adapters/postgres/views.rb b/lib/scenic/adapters/postgres/views.rb index 4c526dd5..d669eefa 100644 --- a/lib/scenic/adapters/postgres/views.rb +++ b/lib/scenic/adapters/postgres/views.rb @@ -99,6 +99,7 @@ def views_from_postgres c.relname as viewname, pg_get_viewdef(c.oid) AS definition, c.relkind AS kind, + c.reloptions AS options, n.nspname AS namespace FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -112,10 +113,29 @@ def views_from_postgres end def to_scenic_view(result) + namespace, viewname, options = result.values_at("namespace", "viewname", "options") + + if options.present? + security_invoker = options.include?("security_invoker=true") + security_barrier = options.include?("security_barrier=true") + end + + options = { + security_invoker:, + security_barrier: + } + + namespaced_viewname = if namespace != "public" + "#{pg_identifier(namespace)}.#{pg_identifier(viewname)}" + else + pg_identifier(viewname) + end + Scenic::View.new( - name: namespaced_view_name(result), + name: namespaced_viewname, definition: result["definition"].strip, - materialized: result["kind"] == "m" + materialized: result["kind"] == "m", + options: ) end diff --git a/lib/scenic/statements.rb b/lib/scenic/statements.rb index 14e1f4a5..0135dafd 100644 --- a/lib/scenic/statements.rb +++ b/lib/scenic/statements.rb @@ -14,6 +14,10 @@ module Statements # @option materialized [Boolean] :no_data (false) Set to true to create # materialized view without running the associated query. You will need # to perform a non-concurrent refresh to populate with data. + # @param security_barrier [Boolean] Set to true to enable the security barrier + # option on the view. Defaults to false. + # @param security_invoker [Boolean] Set to true to enable the security invoker + # option on the view. Defaults to false. # @return The database response from executing the create statement. # # @example Create from `db/views/searches_v02.sql` @@ -24,7 +28,8 @@ module Statements # SELECT * FROM users WHERE users.active = 't' # SQL # - def create_view(name, version: nil, sql_definition: nil, materialized: false) + def create_view(name, version: nil, sql_definition: nil, materialized: false, + security_barrier: false, security_invoker: false) if version.present? && sql_definition.present? raise( ArgumentError, @@ -47,7 +52,7 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false) no_data: options[:no_data] ) else - Scenic.database.create_view(name, sql_definition) + Scenic.database.create_view(name, sql_definition, security_barrier, security_invoker) end end @@ -59,12 +64,16 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false) # `version` argument to {#create_view}. # @param materialized [Boolean] Set to true if dropping a meterialized view. # defaults to false. + # @param security_barrier [Boolean] Set to true to enable the security barrier + # option on the view. Defaults to false. + # @param security_invoker [Boolean] Set to true to enable the security invoker + # option on the view. Defaults to false. # @return The database response from executing the drop statement. # # @example Drop a view, rolling back to version 3 on rollback # drop_view(:users_who_recently_logged_in, revert_to_version: 3) # - def drop_view(name, revert_to_version: nil, materialized: false) + def drop_view(name, revert_to_version: nil, materialized: false, security_barrier: false, security_invoker: false) if materialized Scenic.database.drop_materialized_view(name) else @@ -96,12 +105,17 @@ def drop_view(name, revert_to_version: nil, materialized: false) # The view is initially updated with a temporary name and atomically # swapped once it is successfully created with data. Cannot be combined # with the :no_data option. + # @param security_barrier [Boolean] Set to true to enable the security barrier + # option on the view. Defaults to false. + # @param security_invoker [Boolean] Set to true to enable the security invoker + # option on the view. Defaults to false. # @return The database response from executing the create statement. # # @example # update_view :engagement_reports, version: 3, revert_to_version: 2 # update_view :comments, version: 2, revert_to_version: 1, materialized: { side_by_side: true } - def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false) + def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false, + security_barrier: false, security_invoker: false) if version.blank? && sql_definition.blank? raise( ArgumentError, @@ -139,7 +153,7 @@ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, side_by_side: options[:side_by_side] ) else - Scenic.database.update_view(name, sql_definition) + Scenic.database.update_view(name, sql_definition, security_barrier, security_invoker) end end @@ -154,12 +168,17 @@ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, # @param version [Fixnum] The version number of the view. # @param revert_to_version [Fixnum] The version number to rollback to on # `rake db rollback` + # @param security_barrier [Boolean] Set to true to enable the security barrier + # option on the view. Defaults to false. + # @param security_invoker [Boolean] Set to true to enable the security invoker + # option on the view. Defaults to false. # @return The database response from executing the create statement. # # @example # replace_view :engagement_reports, version: 3, revert_to_version: 2 # - def replace_view(name, version: nil, revert_to_version: nil, materialized: false) + def replace_view(name, version: nil, revert_to_version: nil, materialized: false, + security_barrier: false, security_invoker: false) if version.blank? raise ArgumentError, "version is required" end @@ -170,7 +189,7 @@ def replace_view(name, version: nil, revert_to_version: nil, materialized: false sql_definition = definition(name, version) - Scenic.database.replace_view(name, sql_definition) + Scenic.database.replace_view(name, sql_definition, security_barrier, security_invoker) end private diff --git a/lib/scenic/view.rb b/lib/scenic/view.rb index 3f39d7ed..f969d331 100644 --- a/lib/scenic/view.rb +++ b/lib/scenic/view.rb @@ -22,30 +22,38 @@ class View # @return [Boolean] attr_reader :materialized + # Options definition for security_invoker and security_barrier + # @return Hash[Symbol, Boolean] + attr_reader :options + # Returns a new instance of View. # # @param name [String] The name of the view. # @param definition [String] The SQL for the query that defines the view. # @param materialized [Boolean] `true` if the view is materialized. - def initialize(name:, definition:, materialized:) + def initialize(name:, definition:, materialized:, options:) @name = name @definition = definition @materialized = materialized + @options = options end # @api private def ==(other) name == other.name && definition == other.definition && - materialized == other.materialized + materialized == other.materialized && + options == other.options end # @api private def to_schema materialized_option = materialized ? "materialized: true, " : "" + security_barrier_option = options[:security_barrier] ? "security_barrier: true, " : "" + security_invoker_option = options[:security_invoker] ? "security_invoker: true, " : "" <<-DEFINITION - create_view #{UnaffixedName.for(name).inspect}, #{materialized_option}sql_definition: <<-\SQL + create_view #{UnaffixedName.for(name).inspect}, #{security_barrier_option}#{security_invoker_option}#{materialized_option}sql_definition: <<-\SQL #{escaped_definition.indent(2)} SQL DEFINITION diff --git a/spec/scenic/statements_spec.rb b/spec/scenic/statements_spec.rb index c632f4dd..06a7484e 100644 --- a/spec/scenic/statements_spec.rb +++ b/spec/scenic/statements_spec.rb @@ -22,7 +22,7 @@ module Scenic end it "creates a view from a text definition" do - sql_definition = "a defintion" + sql_definition = "a definition" connection.create_view(:views, sql_definition: sql_definition) @@ -30,7 +30,7 @@ module Scenic .with(:views, sql_definition) end - it "creates version 1 of the view if neither version nor sql_defintion are provided" do + it "creates version 1 of the view if neither version nor sql_definition are provided" do version = 1 definition_stub = instance_double("Definition", to_sql: "foo") allow(Definition).to receive(:new) @@ -43,9 +43,9 @@ module Scenic .with(:views, definition_stub.to_sql) end - it "raises an error if both version and sql_defintion are provided" do + it "raises an error if both version and sql_definition are provided" do expect do - connection.create_view :foo, version: 1, sql_definition: "a defintion" + connection.create_view :foo, version: 1, sql_definition: "a definition" end.to raise_error ArgumentError end end @@ -108,7 +108,7 @@ module Scenic end it "updates a view from a text definition" do - sql_definition = "a defintion" + sql_definition = "a definition" connection.update_view(:name, sql_definition: sql_definition) @@ -160,19 +160,19 @@ module Scenic .with(:name, definition.to_sql, no_data: false, side_by_side: true) end - it "raises an error if not supplied a version or sql_defintion" do + it "raises an error if not supplied a version or sql_definition" do expect { connection.update_view :views }.to raise_error( ArgumentError, /sql_definition or version must be specified/ ) end - it "raises an error if both version and sql_defintion are provided" do + it "raises an error if both version and sql_definition are provided" do expect do connection.update_view( :views, version: 1, - sql_definition: "a defintion" + sql_definition: "a definition" ) end.to raise_error ArgumentError, /cannot both be set/ end From f6b07f50e312278922d530df3ae570916b33eaaf Mon Sep 17 00:00:00 2001 From: Brendan Mulholland Date: Tue, 14 Oct 2025 20:26:08 +0200 Subject: [PATCH 2/4] Drop newer ruby syntax --- lib/generators/scenic/model/model_generator.rb | 1 + lib/generators/scenic/view/view_generator.rb | 1 + lib/scenic/adapters/postgres.rb | 10 +++++----- lib/scenic/adapters/postgres/views.rb | 11 +++++++---- lib/scenic/statements.rb | 6 +++--- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/generators/scenic/model/model_generator.rb b/lib/generators/scenic/model/model_generator.rb index d5796306..55c4540e 100644 --- a/lib/generators/scenic/model/model_generator.rb +++ b/lib/generators/scenic/model/model_generator.rb @@ -8,6 +8,7 @@ module Generators # @api private class ModelGenerator < Rails::Generators::NamedBase include Scenic::Generators::Materializable + source_root File.expand_path("templates", __dir__) def invoke_rails_model_generator diff --git a/lib/generators/scenic/view/view_generator.rb b/lib/generators/scenic/view/view_generator.rb index 555c321d..15133fd6 100644 --- a/lib/generators/scenic/view/view_generator.rb +++ b/lib/generators/scenic/view/view_generator.rb @@ -8,6 +8,7 @@ module Generators class ViewGenerator < Rails::Generators::NamedBase include Rails::Generators::Migration include Scenic::Generators::Materializable + source_root File.expand_path("templates", __dir__) def create_views_directory diff --git a/lib/scenic/adapters/postgres.rb b/lib/scenic/adapters/postgres.rb index 4bca794f..d19dc070 100644 --- a/lib/scenic/adapters/postgres.rb +++ b/lib/scenic/adapters/postgres.rb @@ -63,7 +63,7 @@ def views # @param security_invoker If we should enable security_invoker # # @return [void] - def create_view(name, sql_definition, security_barrier, security_invoker) + def create_view(name, sql_definition, security_barrier: false, security_invoker: false) with_statement = build_with_statement(security_barrier, security_invoker) execute "CREATE VIEW #{quote_table_name(name)} #{with_statement} AS #{sql_definition};" end @@ -86,9 +86,9 @@ def create_view(name, sql_definition, security_barrier, security_invoker) # @param security_invoker If we should enable security_invoker # # @return [void] - def update_view(name, sql_definition, security_barrier, security_invoker) + def update_view(name, sql_definition, security_barrier: false, security_invoker: false) drop_view(name) - create_view(name, sql_definition, security_barrier, security_invoker) + create_view(name, sql_definition, security_barrier: security_barrier, security_invoker: security_invoker) end # Replaces a view in the database using `CREATE OR REPLACE VIEW`. @@ -114,7 +114,7 @@ def update_view(name, sql_definition, security_barrier, security_invoker) # @param security_invoker If we should enable security_invoker # # @return [void] - def replace_view(name, sql_definition, security_barrier, security_invoker) + def replace_view(name, sql_definition, security_barrier: false, security_invoker: false) with_statement = build_with_statement(security_barrier, security_invoker) execute "CREATE OR REPLACE VIEW #{quote_table_name(name)} #{with_statement} AS #{sql_definition};" end @@ -317,7 +317,7 @@ def build_with_statement(security_barrier, security_invoker) return "WITH (security_barrier)" end - return "" + "" end end end diff --git a/lib/scenic/adapters/postgres/views.rb b/lib/scenic/adapters/postgres/views.rb index d669eefa..0a117fbf 100644 --- a/lib/scenic/adapters/postgres/views.rb +++ b/lib/scenic/adapters/postgres/views.rb @@ -115,14 +115,17 @@ def views_from_postgres def to_scenic_view(result) namespace, viewname, options = result.values_at("namespace", "viewname", "options") + security_invoker = false + security_barrier = false + if options.present? - security_invoker = options.include?("security_invoker=true") - security_barrier = options.include?("security_barrier=true") + security_invoker = options.include?("security_invoker=true") + security_barrier = options.include?("security_barrier=true") end options = { - security_invoker:, - security_barrier: + security_invoker: security_invoker, + security_barrier: security_barrier } namespaced_viewname = if namespace != "public" diff --git a/lib/scenic/statements.rb b/lib/scenic/statements.rb index 0135dafd..1bb98df6 100644 --- a/lib/scenic/statements.rb +++ b/lib/scenic/statements.rb @@ -52,7 +52,7 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false, no_data: options[:no_data] ) else - Scenic.database.create_view(name, sql_definition, security_barrier, security_invoker) + Scenic.database.create_view(name, sql_definition, security_barrier: security_barrier, security_invoker: security_invoker) end end @@ -153,7 +153,7 @@ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, side_by_side: options[:side_by_side] ) else - Scenic.database.update_view(name, sql_definition, security_barrier, security_invoker) + Scenic.database.update_view(name, sql_definition, security_barrier: security_barrier, security_invoker: security_invoker) end end @@ -189,7 +189,7 @@ def replace_view(name, version: nil, revert_to_version: nil, materialized: false sql_definition = definition(name, version) - Scenic.database.replace_view(name, sql_definition, security_barrier, security_invoker) + Scenic.database.replace_view(name, sql_definition, security_barrier: security_barrier, security_invoker: security_invoker) end private From 96dc6aa33a8eb85f6ca66c0daa46dd90d477057d Mon Sep 17 00:00:00 2001 From: Brendan Mulholland Date: Tue, 14 Oct 2025 22:42:18 +0200 Subject: [PATCH 3/4] Add tests for new options --- spec/scenic/adapters/postgres/views_spec.rb | 49 +++++++++++++ spec/scenic/adapters/postgres_spec.rb | 41 +++++++++++ spec/scenic/statements_spec.rb | 81 +++++++++++++++++++-- 3 files changed, 165 insertions(+), 6 deletions(-) diff --git a/spec/scenic/adapters/postgres/views_spec.rb b/spec/scenic/adapters/postgres/views_spec.rb index 677fbc52..370d1e3c 100644 --- a/spec/scenic/adapters/postgres/views_spec.rb +++ b/spec/scenic/adapters/postgres/views_spec.rb @@ -18,6 +18,55 @@ module Adapters expect(first.definition).to eq "SELECT 'Elliot'::text AS name;" end + it "returns scenic view objects with security_barrier option" do + connection = ActiveRecord::Base.connection + connection.execute <<-SQL + CREATE VIEW secure_children WITH (security_barrier) AS SELECT text 'Elliot' AS name + SQL + + views = Postgres::Views.new(connection).all + first = views.first + + expect(views.size).to eq 1 + expect(first.name).to eq "secure_children" + expect(first.materialized).to be false + expect(first.definition).to eq "SELECT 'Elliot'::text AS name;" + expect(first.options[:security_barrier]).to eq true + end + + it "returns scenic view objects with security_invoker option" do + connection = ActiveRecord::Base.connection + connection.execute <<-SQL + CREATE VIEW invoker_children WITH (security_invoker = true) AS SELECT text 'Elliot' AS name + SQL + + views = Postgres::Views.new(connection).all + first = views.first + + expect(views.size).to eq 1 + expect(first.name).to eq "invoker_children" + expect(first.materialized).to be false + expect(first.definition).to eq "SELECT 'Elliot'::text AS name;" + expect(first.options[:security_invoker]).to eq true + end + + it "returns scenic view objects with both security options" do + connection = ActiveRecord::Base.connection + connection.execute <<-SQL + CREATE VIEW secure_invoker_children WITH (security_barrier, security_invoker = true) AS SELECT text 'Elliot' AS name + SQL + + views = Postgres::Views.new(connection).all + first = views.first + + expect(views.size).to eq 1 + expect(first.name).to eq "secure_invoker_children" + expect(first.materialized).to be false + expect(first.definition).to eq "SELECT 'Elliot'::text AS name;" + expect(first.options[:security_barrier]).to eq true + expect(first.options[:security_invoker]).to eq true + end + it "returns scenic view objects for materialized views" do connection = ActiveRecord::Base.connection connection.execute <<-SQL diff --git a/spec/scenic/adapters/postgres_spec.rb b/spec/scenic/adapters/postgres_spec.rb index f95529e1..1dd44717 100644 --- a/spec/scenic/adapters/postgres_spec.rb +++ b/spec/scenic/adapters/postgres_spec.rb @@ -11,6 +11,34 @@ module Adapters expect(adapter.views.map(&:name)).to include("greetings") end + + it "successfully creates a view with security_barrier" do + adapter = Postgres.new + + adapter.create_view("greetings", "SELECT text 'hi' AS greeting", security_barrier: true) + + view = adapter.views.find { |v| v.name == "greetings" } + expect(view.options[:security_barrier]).to eq(true) + end + + it "successfully creates a view with security_invoker" do + adapter = Postgres.new + + adapter.create_view("greetings", "SELECT text 'hi' AS greeting", security_invoker: true) + + view = adapter.views.find { |v| v.name == "greetings" } + expect(view.options[:security_invoker]).to eq(true) + end + + it "successfully creates a view with both security_barrier and security_invoker" do + adapter = Postgres.new + + adapter.create_view("greetings", "SELECT text 'hi' AS greeting", security_barrier: true, security_invoker: true) + + view = adapter.views.find { |v| v.name == "greetings" } + expect(view.options[:security_barrier]).to eq(true) + expect(view.options[:security_invoker]).to eq(true) + end end describe "#create_materialized_view" do @@ -66,6 +94,19 @@ module Adapters view = adapter.views.first.definition expect(view).to eql "SELECT 'hello'::text AS greeting;" end + + it "successfully replaces a view with security options" do + adapter = Postgres.new + + adapter.create_view("greetings", "SELECT text 'hi' AS greeting") + + adapter.replace_view("greetings", "SELECT text 'hello' AS greeting", security_barrier: true, security_invoker: true) + + view = adapter.views.find { |v| v.name == "greetings" } + expect(view.definition).to eql "SELECT 'hello'::text AS greeting;" + expect(view.options[:security_barrier]).to eq(true) + expect(view.options[:security_invoker]).to eq(true) + end end describe "#drop_view" do diff --git a/spec/scenic/statements_spec.rb b/spec/scenic/statements_spec.rb index 06a7484e..047d8084 100644 --- a/spec/scenic/statements_spec.rb +++ b/spec/scenic/statements_spec.rb @@ -18,7 +18,7 @@ module Scenic connection.create_view :views, version: version expect(Scenic.database).to have_received(:create_view) - .with(:views, definition_stub.to_sql) + .with(:views, definition_stub.to_sql, security_barrier: false, security_invoker: false) end it "creates a view from a text definition" do @@ -27,7 +27,7 @@ module Scenic connection.create_view(:views, sql_definition: sql_definition) expect(Scenic.database).to have_received(:create_view) - .with(:views, sql_definition) + .with(:views, sql_definition, security_barrier: false, security_invoker: false) end it "creates version 1 of the view if neither version nor sql_definition are provided" do @@ -40,7 +40,34 @@ module Scenic connection.create_view :views expect(Scenic.database).to have_received(:create_view) - .with(:views, definition_stub.to_sql) + .with(:views, definition_stub.to_sql, security_barrier: false, security_invoker: false) + end + + it "creates a view with security_barrier" do + sql_definition = "a definition" + + connection.create_view(:views, sql_definition: sql_definition, security_barrier: true) + + expect(Scenic.database).to have_received(:create_view) + .with(:views, sql_definition, hash_including(security_barrier: true)) + end + + it "creates a view with security_invoker" do + sql_definition = "a definition" + + connection.create_view(:views, sql_definition: sql_definition, security_invoker: true) + + expect(Scenic.database).to have_received(:create_view) + .with(:views, sql_definition, hash_including(security_invoker: true)) + end + + it "creates a view with both security options" do + sql_definition = "a definition" + + connection.create_view(:views, sql_definition: sql_definition, security_barrier: true, security_invoker: true) + + expect(Scenic.database).to have_received(:create_view) + .with(:views, sql_definition, security_barrier: true, security_invoker: true) end it "raises an error if both version and sql_definition are provided" do @@ -104,7 +131,7 @@ module Scenic connection.update_view(:name, version: 3) expect(Scenic.database).to have_received(:update_view) - .with(:name, definition.to_sql) + .with(:name, definition.to_sql, security_barrier: false, security_invoker: false) end it "updates a view from a text definition" do @@ -113,7 +140,25 @@ module Scenic connection.update_view(:name, sql_definition: sql_definition) expect(Scenic.database).to have_received(:update_view) - .with(:name, sql_definition) + .with(:name, sql_definition, security_barrier: false, security_invoker: false) + end + + it "updates a view with security_barrier" do + sql_definition = "a definition" + + connection.update_view(:name, sql_definition: sql_definition, security_barrier: true) + + expect(Scenic.database).to have_received(:update_view) + .with(:name, sql_definition, hash_including(security_barrier: true)) + end + + it "updates a view with security_invoker" do + sql_definition = "a definition" + + connection.update_view(:name, sql_definition: sql_definition, security_invoker: true) + + expect(Scenic.database).to have_received(:update_view) + .with(:name, sql_definition, hash_including(security_invoker: true)) end it "updates the materialized view in the database" do @@ -218,7 +263,31 @@ module Scenic connection.replace_view(:name, version: 3) expect(Scenic.database).to have_received(:replace_view) - .with(:name, definition.to_sql) + .with(:name, definition.to_sql, security_barrier: false, security_invoker: false) + end + + it "replaces a view with security_barrier" do + definition = instance_double("Definition", to_sql: "definition") + allow(Definition).to receive(:new) + .with(:name, 3) + .and_return(definition) + + connection.replace_view(:name, version: 3, security_barrier: true) + + expect(Scenic.database).to have_received(:replace_view) + .with(:name, definition.to_sql, hash_including(security_barrier: true)) + end + + it "replaces a view with security_invoker" do + definition = instance_double("Definition", to_sql: "definition") + allow(Definition).to receive(:new) + .with(:name, 3) + .and_return(definition) + + connection.replace_view(:name, version: 3, security_invoker: true) + + expect(Scenic.database).to have_received(:replace_view) + .with(:name, definition.to_sql, hash_including(security_invoker: true)) end it "fails to replace the materialized view in the database" do From 93b39f68f9067a9af80e21ac47980d06b09e21f2 Mon Sep 17 00:00:00 2001 From: Caleb Hearth Date: Thu, 13 Nov 2025 13:20:42 -0700 Subject: [PATCH 4/4] Allow create/update/replace_view to accept with: Postgres accepts arguments in the form `WITH (