Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions contrib/babelfishpg_tsql/sql/sys_function_helpers.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11159,7 +11159,7 @@ DECLARE
counter int;
cur_pos int;
BEGIN
lower_object_name = pg_catalog.lower(PG_CATALOG.rtrim(name));
lower_object_name = pg_catalog.lower(name);

counter = 1;
cur_pos = babelfish_get_name_delimiter_pos(lower_object_name);
Expand All @@ -11171,7 +11171,7 @@ BEGIN
RETURN;
END IF;

names[counter] = babelfish_remove_delimiter_pair(PG_CATALOG.rtrim(PG_CATALOG.left(lower_object_name, cur_pos - 1)));
names[counter] = babelfish_remove_delimiter_pair(PG_CATALOG.left(lower_object_name, cur_pos - 1));

-- invalid name
IF names[counter] IS NULL THEN
Expand All @@ -11198,7 +11198,7 @@ BEGIN
END CASE;

-- Assign each name accordingly
object_name = sys.babelfish_truncate_identifier(babelfish_remove_delimiter_pair(PG_CATALOG.rtrim(lower_object_name)));
object_name = sys.babelfish_truncate_identifier(babelfish_remove_delimiter_pair(lower_object_name));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this change produce a regression for case when originally database name do not have spaces but we passed spaces while calling HAS_PERMS_BY_NAME function in database name, eg:

create database [hello_db]
go

SELECT HAS_PERMS_BY_NAME('hello_db    ','DATABASE','CREATE FUNCTION');

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the old test and my tests combination support that test you wanted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that tests are there but is the behaviour correct ?
Can we confirm this behaviour with t-sql behaviour once ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand you want original mssql behaviour and I mentioned before with this test:

image

Isn't this confirmation that you wanted?

Copy link
Contributor

@pranavJ23 pranavJ23 Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me clarify it again :

  1. Make a db without any trailing spaces:
create database [hello_db]
go
  1. call function as
SELECT HAS_PERMS_BY_NAME('hello_db    ','DATABASE','CREATE FUNCTION');

Now we are calling the HAS_PERMS_BY_NAME with database as hello_db with trailing spaces , now this should return 1 as output as per t-sql behaviour, but since with your change you are removing the rtrim() so this could make the result 0 (instead of 1).

Note: in the tests you added contain the database name with spaces and we are comparing that, in the test I am pointing towards, database name does not have any spaces in it's name, but while calling HAS_PERMS_BY_NAME function we are adding spaces in database name

END;
$$
LANGUAGE plpgsql
Expand Down
2 changes: 1 addition & 1 deletion contrib/babelfishpg_tsql/sql/sys_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2994,7 +2994,7 @@ BEGIN

-- Lower-case to avoid case issues, remove trailing whitespace to match SQL SERVER behavior
-- Objects created in Babelfish are stored in lower-case in pg_class/pg_proc
cs_as_securable = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_securable));
cs_as_securable = pg_catalog.lower(cs_as_securable);
cs_as_securable_class = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_securable_class));
cs_as_permission = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_permission));
cs_as_sub_securable = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_sub_securable));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,329 @@ $$;
CALL sys.analyze_babelfish_catalogs();
-- Reset search_path to not affect any subsequent scripts
SELECT set_config('search_path', trim(leading 'sys, ' from current_setting('search_path')), false);


-- valid names are db_name.schema_name.object_name or schema_name.object_name or object_name
CREATE OR REPLACE FUNCTION sys.babelfish_split_object_name(
name TEXT,
OUT db_name TEXT,
OUT schema_name TEXT,
OUT object_name TEXT)
AS $$
DECLARE
lower_object_name text;
names text[2];
counter int;
cur_pos int;
BEGIN
lower_object_name = pg_catalog.lower(name);

counter = 1;
cur_pos = babelfish_get_name_delimiter_pos(lower_object_name);

-- Parse user input into names split by '.'
WHILE cur_pos > 0 LOOP
IF counter > 3 THEN
-- Too many names provided
RETURN;
END IF;

names[counter] = babelfish_remove_delimiter_pair(PG_CATALOG.left(lower_object_name, cur_pos - 1));

-- invalid name
IF names[counter] IS NULL THEN
RETURN;
END IF;

lower_object_name = substring(lower_object_name from cur_pos + 1);
counter = counter + 1;
cur_pos = babelfish_get_name_delimiter_pos(lower_object_name);
END LOOP;

CASE counter
WHEN 1 THEN
db_name = NULL;
schema_name = NULL;
WHEN 2 THEN
db_name = NULL;
schema_name = sys.babelfish_truncate_identifier(names[1]);
WHEN 3 THEN
db_name = sys.babelfish_truncate_identifier(names[1]);
schema_name = sys.babelfish_truncate_identifier(names[2]);
ELSE
RETURN;
END CASE;

-- Assign each name accordingly
object_name = sys.babelfish_truncate_identifier(babelfish_remove_delimiter_pair(lower_object_name));
END;
$$
LANGUAGE plpgsql
STABLE;

CREATE OR REPLACE FUNCTION sys.has_perms_by_name(
securable SYS.SYSNAME,
securable_class SYS.NVARCHAR(60),
permission SYS.SYSNAME,
sub_securable SYS.SYSNAME DEFAULT NULL,
sub_securable_class SYS.NVARCHAR(60) DEFAULT NULL
)
RETURNS integer
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
db_name text COLLATE sys.database_default;
bbf_schema_name text COLLATE sys.database_default;
pg_schema text COLLATE sys.database_default;
implied_dbo_permissions boolean;
fully_supported boolean;
is_cross_db boolean := false;
object_name text COLLATE sys.database_default;
database_id smallint;
namespace_id oid;
userid oid;
object_type text;
function_signature text;
qualified_name text;
return_value integer;
cs_as_securable text COLLATE "C" := securable;
cs_as_securable_class text COLLATE "C" := securable_class;
cs_as_permission text COLLATE "C" := permission;
cs_as_sub_securable text COLLATE "C" := sub_securable;
cs_as_sub_securable_class text COLLATE "C" := sub_securable_class;
BEGIN
return_value := NULL;

-- Lower-case to avoid case issues, remove trailing whitespace to match SQL SERVER behavior
-- Objects created in Babelfish are stored in lower-case in pg_class/pg_proc
cs_as_securable = pg_catalog.lower(cs_as_securable);
cs_as_securable_class = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_securable_class));
cs_as_permission = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_permission));
cs_as_sub_securable = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_sub_securable));
cs_as_sub_securable_class = pg_catalog.lower(PG_CATALOG.rtrim(cs_as_sub_securable_class));

-- Assert that sub_securable and sub_securable_class are either both NULL or both defined
IF cs_as_sub_securable IS NOT NULL AND cs_as_sub_securable_class IS NULL THEN
RETURN NULL;
ELSIF cs_as_sub_securable IS NULL AND cs_as_sub_securable_class IS NOT NULL THEN
RETURN NULL;
-- If they are both defined, user must be evaluating column privileges.
-- Check that inputs are valid for column privileges: sub_securable_class must
-- be column, securable_class must be object, and permission cannot be any.
ELSIF cs_as_sub_securable_class IS NOT NULL
AND (cs_as_sub_securable_class != 'column'
OR cs_as_securable_class IS NULL
OR cs_as_securable_class != 'object'
OR cs_as_permission = 'any') THEN
RETURN NULL;

-- If securable is null, securable_class must be null
ELSIF cs_as_securable IS NULL AND cs_as_securable_class IS NOT NULL THEN
RETURN NULL;
-- If securable_class is null, securable must be null
ELSIF cs_as_securable IS NOT NULL AND cs_as_securable_class IS NULL THEN
RETURN NULL;
END IF;

IF cs_as_securable_class = 'server' THEN
-- SQL Server does not permit a securable_class value of 'server'.
-- securable_class should be NULL to evaluate server permissions.
RETURN NULL;
ELSIF cs_as_securable_class IS NULL THEN
-- NULL indicates a server permission. Set this variable so that we can
-- search for the matching entry in babelfish_has_perms_by_name_permissions
cs_as_securable_class = 'server';
END IF;

IF cs_as_sub_securable IS NOT NULL THEN
cs_as_sub_securable := babelfish_remove_delimiter_pair(cs_as_sub_securable);
IF cs_as_sub_securable IS NULL THEN
RETURN NULL;
END IF;
END IF;

SELECT p.implied_dbo_permissions,p.fully_supported
INTO implied_dbo_permissions,fully_supported
FROM babelfish_has_perms_by_name_permissions p
WHERE p.securable_type = cs_as_securable_class AND p.permission_name = cs_as_permission;

IF implied_dbo_permissions IS NULL OR fully_supported IS NULL THEN
-- Securable class or permission is not valid, or permission is not valid for given securable
RETURN NULL;
END IF;

IF cs_as_securable_class = 'database' AND cs_as_securable IS NOT NULL THEN
db_name = babelfish_remove_delimiter_pair(cs_as_securable);
IF db_name IS NULL THEN
RETURN NULL;
ELSIF (SELECT COUNT(name) FROM sys.databases WHERE name = db_name COLLATE sys.database_default) != 1 THEN
RETURN 0;
END IF;
ELSIF cs_as_securable_class = 'schema' THEN
bbf_schema_name = babelfish_remove_delimiter_pair(cs_as_securable);
IF bbf_schema_name IS NULL THEN
RETURN NULL;
ELSIF (SELECT COUNT(nspname) FROM sys.babelfish_namespace_ext ext
WHERE ext.orig_name = bbf_schema_name COLLATE sys.database_default
AND ext.dbid = sys.db_id()) != 1 THEN
RETURN 0;
END IF;
END IF;

IF fully_supported = 'f' AND
(SELECT orig_username FROM sys.babelfish_authid_user_ext WHERE rolname = CURRENT_USER) = 'dbo' THEN
RETURN CAST(implied_dbo_permissions AS integer);
ELSIF fully_supported = 'f' THEN
RETURN 0;
END IF;

-- The only permissions that are fully supported belong to the OBJECT securable class.
-- The block above has dealt with all permissions that are not fully supported, so
-- if we reach this point we know the securable class is OBJECT.
SELECT s.db_name, s.schema_name, s.object_name INTO db_name, bbf_schema_name, object_name
FROM babelfish_split_object_name(cs_as_securable) s;

-- Invalid securable name
IF object_name IS NULL OR object_name = '' THEN
RETURN NULL;
END IF;

-- If schema was not specified, use the default
IF bbf_schema_name IS NULL OR bbf_schema_name = '' THEN
bbf_schema_name := sys.schema_name();
END IF;

database_id := (
SELECT CASE
WHEN db_name IS NULL OR db_name = '' THEN (sys.db_id())
ELSE (sys.db_id(db_name))
END);

IF database_id <> sys.db_id() THEN
is_cross_db = true;
END IF;

userid := (
SELECT CASE
WHEN is_cross_db THEN sys.suser_id()
ELSE sys.user_id()
END);

-- Translate schema name from bbf to postgres, e.g. dbo -> master_dbo
pg_schema := (SELECT nspname
FROM sys.babelfish_namespace_ext ext
WHERE ext.orig_name = bbf_schema_name COLLATE sys.database_default
AND CAST(ext.dbid AS oid) = CAST(database_id AS oid));

IF pg_schema IS NULL THEN
-- Shared schemas like sys and pg_catalog do not exist in the table above.
-- These schemas do not need to be translated from Babelfish to Postgres
pg_schema := bbf_schema_name;
END IF;

-- Surround with double-quotes to handle names that contain periods/spaces
qualified_name := PG_CATALOG.concat('"', pg_schema, '"."', object_name, '"');

SELECT oid INTO namespace_id FROM pg_catalog.pg_namespace WHERE nspname = pg_schema COLLATE sys.database_default;

object_type := (
SELECT CASE
WHEN cs_as_sub_securable_class = 'column'
THEN CASE
WHEN (SELECT count(a.attname)
FROM pg_attribute a
INNER JOIN pg_class c ON c.oid = a.attrelid
INNER JOIN pg_namespace s ON s.oid = c.relnamespace
WHERE
a.attname = cs_as_sub_securable COLLATE sys.database_default
AND c.relname = object_name COLLATE sys.database_default
AND s.nspname = pg_schema COLLATE sys.database_default
AND NOT a.attisdropped
AND (s.nspname IN (SELECT nspname FROM sys.babelfish_namespace_ext) OR s.nspname = 'sys')
-- r = ordinary table, i = index, S = sequence, t = TOAST table, v = view, m = materialized view, c = composite type, f = foreign table, p = partitioned table
AND c.relkind IN ('r', 'v', 'm', 'f', 'p')
AND a.attnum > 0) = 1
THEN 'column'
ELSE NULL
END

WHEN (SELECT count(relname)
FROM pg_catalog.pg_class
WHERE relname = object_name COLLATE sys.database_default
AND relnamespace = namespace_id) = 1
THEN 'table'

WHEN (SELECT count(proname)
FROM pg_catalog.pg_proc
WHERE proname = object_name COLLATE sys.database_default
AND pronamespace = namespace_id
AND prokind = 'f') = 1
THEN 'function'

WHEN (SELECT count(proname)
FROM pg_catalog.pg_proc
WHERE proname = object_name COLLATE sys.database_default
AND pronamespace = namespace_id
AND prokind = 'p') = 1
THEN 'procedure'
ELSE NULL
END
);

-- Object was not found
IF object_type IS NULL THEN
RETURN 0;
END IF;

-- Get signature for function-like objects
IF object_type IN('function', 'procedure') THEN
SELECT CAST(oid AS regprocedure)
INTO function_signature
FROM pg_catalog.pg_proc
WHERE proname = object_name COLLATE sys.database_default
AND pronamespace = namespace_id;
END IF;

return_value := (
SELECT CASE
WHEN cs_as_permission = 'any' THEN babelfish_has_any_privilege(userid, object_type, pg_schema, object_name)

WHEN object_type = 'column'
THEN CASE
WHEN cs_as_permission IN('insert', 'delete', 'execute') THEN NULL
ELSE CAST(has_column_privilege(userid, qualified_name, cs_as_sub_securable, cs_as_permission) AS integer)
END

WHEN object_type = 'table'
THEN CASE
WHEN cs_as_permission = 'execute' THEN 0
ELSE CAST(has_table_privilege(userid, qualified_name, cs_as_permission) AS integer)
END

WHEN object_type = 'function'
THEN CASE
WHEN cs_as_permission IN('select', 'execute')
THEN CAST(has_function_privilege(userid, function_signature, 'execute') AS integer)
WHEN cs_as_permission IN('update', 'insert', 'delete', 'references')
THEN 0
ELSE NULL
END

WHEN object_type = 'procedure'
THEN CASE
WHEN cs_as_permission = 'execute'
THEN CAST(has_function_privilege(userid, function_signature, 'execute') AS integer)
WHEN cs_as_permission IN('select', 'update', 'insert', 'delete', 'references')
THEN 0
ELSE NULL
END

ELSE NULL
END
);

RETURN return_value;
EXCEPTION WHEN OTHERS THEN RETURN NULL;
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--cleanup databases
DROP DATABASE IF EXISTS [ blank space ];
GO
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
--drop databases if exists
DROP DATABASE IF EXISTS [ blank space ];
GO

--create databases
CREATE DATABASE [ blank space ];
GO
Loading
Loading