Skryptowanie w SQL Server 2008

[PL] Skryptowanie w SQL Server 2008 – Więzy DEFAULT i CHECK

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Intro

Kontynuuję serię poświęconą skryptowaniu obiektów w SQL Server 2008. Zapoznaj się także z poprzednimi wpisami z tej serii:

Dziś pokażę, jak można skryptować więzy wartości domyślnych (DEFAULT) i ograniczeń wartości (CHECK). Dla przypomnienia – więzy DEFAULT służą do narzucania kolumnom wartości domyślnych (gdy użytkownik nie wstawia jawnie danych w kolumnę, system wstawia wartość domyślną), zaś więzy CHECK służą do narzucenia ograniczeń wartości, jakie mogą przyjmować dane w kolumnie (stąd więzy te służą m.in. do tworzenia odpowiedników enumeracji w kolumnach w SQL Server).

Skryptowanie więzów DEFAULT

Zadanie: zeskryptować wszystkie więzy DEFAULT w wybranej bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
SELECT
   'IF OBJECT_ID(N''' +
    REPLACE(
      QUOTENAME(SCHEMA_NAME(d.[schema_id])) + '.' + QUOTENAME(d.name),
      '''', ''''''
    ) + ''', N''D'') IS NULL
  ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(d.parent_object_id)) + '.' +
  QUOTENAME(OBJECT_NAME(d.parent_object_id)) + '
  ADD CONSTRAINT ' + QUOTENAME(d.name) + '
  DEFAULT ' + d.definition + ' FOR ' + QUOTENAME(c.name) + ';
GO'
FROM sys.default_constraints AS d
INNER JOIN sys.columns AS c
ON d.parent_object_id = c.[object_id]
AND d.parent_column_id = c.column_id
WHERE is_ms_shipped = 0;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • metadane więzów DEFAULT zwraca widok systemowy sys.default_constraints,
  • nazwy kolumn zwraca widok systemowy sys.columns (jest też widok sys.all_columns, który zawiera także kolumny obiektów systemowych),
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi,
  • skryptuję tylko obiekty, które zostały stworzone przez użytkowników (filtr na kolumnie is_ms_shipped).

Skryptowanie więzów CHECK

Zadanie: zeskryptować wszystkie więzy CHECK w wybranej bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
SELECT
  'IF OBJECT_ID(N''' +
    REPLACE(
      QUOTENAME(SCHEMA_NAME([schema_id])) + '.' + QUOTENAME(name),
      '''', ''''''
    ) + ''', N''C'') IS NULL
  ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' +
  QUOTENAME(OBJECT_NAME(parent_object_id)) + '
  ADD CONSTRAINT ' + QUOTENAME(name) + '
  CHECK ' + CASE is_not_for_replication WHEN 1 THEN 'NOT FOR REPLICATION ' ELSE '' END +
  definition + ';
GO'
FROM sys.check_constraints
WHERE is_ms_shipped = 0;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • metadane więzów DEFAULT zwraca widok systemowy sys.check_constraints,
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi,
  • skryptuję tylko obiekty, które zostały stworzone przez użytkowników (filtr na kolumnie is_ms_shipped).

Podsumowanie

Skryptowanie więzów DEFAULT i CHECK jest moim zdaniem stosunkowo proste. Definicje wyrażeń determinujących działanie tych więzów są zapisane w pojedynczych kolumnach. W zasadzie sprawa sprowadza się do zbudowania odpowiedniej składni T-SQL, a to jest bodaj najprostsze. Należy pamiętać, że przed utworzeniem więzów DEFAULT i CHECK musimy zadbać, by w bazie danych istniały wszystkie funkcje skalarne, jakie owe więzy wykorzystują.

[PL] Skryptowanie w SQL Server 2008 – Klucze obce

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Intro

To już trzeci wpis z serii “Skryptowanie w SQL Server 2008″. Dwa poprzednie dostępne są tu:

Tym razem przedstawiam sposób na skryptowanie kluczy obcych. Kiedy takie skryptowanie może się przydać? Na przykład wtedy, gdy chcemy usunąć, a następnie odtworzyć owe klucze (typowy scenariusz: import wyczyszczonych i zwalidowanych danych). W podsumowaniu podam, jakie rozwinięcia mojego kodu można zaimplementować, by za pomocą praktycznie tego samego kodu wykonywać inne zadania związane z kluczami obcymi.

Skryptowanie kluczy obcych

Zadanie: zeskryptować wszystkie klucze obce w bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;

-- tu będziemy trzymać generowany kod
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);

DECLARE
  @object_name sysname,
  @object_id int,
  @object_schema_name sysname,
  @parent_object_name sysname,
  @parent_object_schema_name sysname,
  @is_not_for_replication bit,
  @delete_referential_action int,
  @update_referential_action int,
  @referenced_object_schema_name sysname,
  @referenced_object_name sysname,
  @column_name sysname,
  @referenced_column_name sysname,
  @columns nvarchar(4000),
  @referenced_columns nvarchar(4000);

-- kursor do wyciągania pojedynczych kluczy obcych
DECLARE CursorFK CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    name,
    [object_id],
    SCHEMA_NAME([schema_id]),
    OBJECT_SCHEMA_NAME(parent_object_id),
    OBJECT_NAME(parent_object_id),
    OBJECT_SCHEMA_NAME(referenced_object_id),
    OBJECT_NAME(referenced_object_id),
    is_not_for_replication,
    delete_referential_action,
    update_referential_action
  FROM sys.foreign_keys
  WHERE is_ms_shipped = 0;
OPEN CursorFK;
FETCH NEXT FROM CursorFK
INTO @object_name, @object_id, @object_schema_name,
     @parent_object_schema_name, @parent_object_name,
     @referenced_object_schema_name, @referenced_object_name,
     @is_not_for_replication,
     @delete_referential_action, @update_referential_action;

WHILE @@FETCH_STATUS = 0 BEGIN
  SELECT @columns = '', @referenced_columns = '';

  -- generujemy sprawdzenie, czy klucz nie istnieje
  INSERT INTO @SQL (Line)
  SELECT 'IF OBJECT_ID(N''' +
        REPLACE(
          QUOTENAME(@object_schema_name) + '.' +
          QUOTENAME(@object_name), '''', ''''''
        ) + ''', N''F'') IS NULL';

  -- generujemy polecenie ALTER TABLE ... ADD CONSTRAINT
  INSERT INTO @SQL (Line)
  SELECT '  ALTER TABLE ' +
    QUOTENAME(@parent_object_schema_name) + '.' +
    QUOTENAME(@parent_object_name);
  INSERT INTO @SQL (Line)
  SELECT '  ADD CONSTRAINT ' + QUOTENAME(@object_name);

  -- kursor do wyciągania kolejnych kolumn klucza obcego
  DECLARE CursorFKColumns CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR
    SELECT
      pc.name,
      rc.name
    FROM sys.foreign_key_columns AS f
    INNER JOIN sys.columns AS pc
    ON f.parent_object_id = pc.[object_id]
    AND f.parent_column_id = pc.column_id
    INNER JOIN sys.columns AS rc
    ON f.referenced_object_id = rc.[object_id]
    AND f.referenced_column_id = rc.column_id
    WHERE f.constraint_object_id = @object_id
    ORDER BY f.constraint_column_id
  OPEN CursorFKColumns;
  FETCH NEXT FROM CursorFKColumns
  INTO
    @column_name,
    @referenced_column_name;
  WHILE @@FETCH_STATUS = 0 BEGIN
    -- doklejamy do zestawów kolumn
    SET @columns = @columns + ', ' + QUOTENAME(@column_name);
    SET @referenced_columns = @referenced_columns + ', ' + QUOTENAME(@referenced_column_name);

    FETCH NEXT FROM CursorFKColumns
    INTO
      @column_name,
      @referenced_column_name;
  END;
  CLOSE CursorFKColumns;
  DEALLOCATE CursorFKColumns;

  -- generujemy kod z listami kolumn
  INSERT INTO @SQL (Line)
  SELECT '  FOREIGN KEY (' + STUFF(@columns, 1, 2, '') + ')';
  INSERT INTO @SQL (Line)
  SELECT '  REFERENCES ' +
    QUOTENAME(@referenced_object_schema_name) + '.' +
    QUOTENAME(@referenced_object_name) + '(' +
    STUFF(@referenced_columns, 1, 2, '') + ')';

  -- akcje klucza przy usuwaniu wierszy
  IF @delete_referential_action <> 0 BEGIN
    INSERT INTO @SQL (Line)
    SELECT '  ON DELETE ' +
      CASE @delete_referential_action
        WHEN 1 THEN 'CASCADE'
        WHEN 2 THEN 'SET NULL'
        WHEN 3 THEN 'SET DEFAULT'
      END;
  END;

  -- akcje klucza przy modyfikacji wierszy
  IF @update_referential_action <> 0 BEGIN
    INSERT INTO @SQL (Line)
    SELECT '  ON UPDATE ' +
      CASE @update_referential_action
        WHEN 1 THEN 'CASCADE'
        WHEN 2 THEN 'SET NULL'
        WHEN 3 THEN 'SET DEFAULT'
      END;
  END;

  -- czy wymuszamy regułę klucza w replikacjach
  IF @is_not_for_replication = 1
    INSERT INTO @SQL (Line)
    SELECT '  NOT FOR REPLICATION';

  UPDATE @SQL
  SET Line = Line + ';'
  WHERE LineId = SCOPE_IDENTITY();

  INSERT INTO @SQL (Line) SELECT 'GO';

  FETCH NEXT FROM CursorFK
  INTO @object_name, @object_id, @object_schema_name,
     @parent_object_schema_name, @parent_object_name,
     @referenced_object_schema_name, @referenced_object_name,
     @is_not_for_replication,
     @delete_referential_action, @update_referential_action;
END;
CLOSE CursorFK;
DEALLOCATE CursorFK;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • metadane kluczy obcych zwracają widoki sys.foreign_keys (metadane tabeli, w której tworzymy klucz, opcje dla poleceń DELETE / UPDATE, opcja wyłączenia klucza dla replikacji) oraz sys.foreign_key_columns (metadane kolumn oraz tabeli, do której odwołuje się klucz obcy),
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi i apostrofami,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • rozwiązanie oparłem o dwa kursory: CursorFK – do iterowania po kluczach obcych oraz zagnieżdżony CursorFKColumns – do iterowania po kolumnach bieżącego klucza,
  • ponieważ nie jestem zwolennikiem wyłączania więzów, moje rozwiązanie nie uwzględnia kolumny is_disabled z widoku sys.foreign_keys (nie generuję klauzuli WITH NOCHECK nawet, gdy klucz jest wyłączony).

Słowo podsumowania

Powyższy kod można rozwinąć:

  • można dołożyć filtrowanie po tabelach (czy to tabeli, w której klucz jest utworzony, czy po tabeli, do której klucz się odwołuje),
  • można generować tylko składnie ALTER TABLE … DROP CONSTRAINT, jeżeli zależy nam tylko na usunięciu wybranych kluczy,
  • można wykorzystać kolumnę is_system_named z widoku sys.foreign_keys, jeżeli chcemy odnaleźć te klucze, które otrzymały nazwy wygenerowane systemowo (jeżeli nie chcemy mieć w bazie danych obiektów nazywanych dość przypadkowo).

Na koniec informacja – w repozytorium kodu umieściłem kod pseudo-systemowej procedury sp_dropforeignkeys, która może posłużyć do wygenerowania kodu usuwającego klucze obce wskazujące na wybraną tabelę. W procedurze tej wykorzystałem te same obiekty systemowe, co w powyższym kodzie, więc komentarz do niej wydaje mi się zbędny. Po stworzeniu w bazie master i oznaczeniu jako obiekt systemowy procedura powinna działać w kontekście dowolnej bazy danych.

[PL] Skryptowanie w SQL Server 2008 – Proste typy danych użytkownika

VN:F [1.7.9_1023]
Rating: 5.0/5 (1 vote cast)

Intro

Typy danych użytkownika tworzymy zazwyczaj z myślą, by ewentualne zmiany długości, precyzji czy skali przeprowadzać w sposób prostszy, bo scentralizowany. Niestety, Microsoft nie daje nam w SQL Server polecenia ALTER TYPE, ale i tak używamy własnych typów, a w razie potrzeby jakiejkolwiek zmiany przeprowadzamy operację opisaną tutaj: [EN] Call for voting – ALTER TYPE in SQL Server. W tej notce pokażę, jak można skryptować proste typy danych stworzone przez użytkowników w bazie danych. Nie będę opisywał skryptowania typów tabelarycznych i typów stworzonych w CLR.

Skryptowanie typów prostych

Zadanie: zeskryptować wszystkie proste typy danych użytkownika w bazie danych.

Dla każdego typu trzeba stworzyć kod sprawdzający, czy typ nie istnieje (trzeba zajrzeć do widoku sys.types) oraz wygenerować kod polecenia CREATE TYPE o składni jak poniżej (na podstawie Books Online):

CREATE TYPE [ schema_name. ] type_name
FROM base_type   [ ( precision [ , scale ] ) ] [ NULL | NOT NULL ];

Rozwiązanie:

USE AdventureWorks2008R2;
SET NOCOUNT ON;
SELECT
  'IF NOT EXISTS (
  SELECT * FROM sys.types
  WHERE [schema_id] = SCHEMA_ID(' +
  QUOTENAME(SCHEMA_NAME([schema_id]), '''') + ') AND name = N' +
  QUOTENAME(name, '''') + '
)
  CREATE TYPE ' + QUOTENAME(SCHEMA_NAME([schema_id])) + '.' + QUOTENAME(name) + '
  FROM ' + TYPE_NAME(system_type_id) +
  CASE
    WHEN system_type_id IN (
      165, --varbinary
      167, --varchar
      173, --binary
      175, --char
      231, --nvarchar
      239  --nchar
    )
      THEN '(' +
        CONVERT(
          varchar(4),
          CASE
            WHEN max_length = -1
              THEN 'max'
            ELSE max_length
          END
        ) + ')'
    WHEN system_type_id IN (
      41, --time
      42, --datetime2
      43  --datetimeoffset
    )
      THEN '(' + CONVERT(char(1), scale) + ')'
    WHEN system_type_id IN (
      106, --decimal
      108  --numeric
    )
      THEN '(' +
        CONVERT(varchar(2), precision) + ',' +
        CONVERT(varchar(2), scale) + ')'
    WHEN system_type_id = 62 --float
      THEN '(' + CONVERT(varchar(2), precision) + ')'
    ELSE ''
  END + ' ' +
  CASE
    WHEN is_nullable = 0
      THEN 'NOT NULL'
    ELSE 'NULL'
  END + ';
GO'
FROM sys.types
WHERE is_user_defined = 1 --typ użytkownika
AND is_table_type = 0     --nie typ tabelaryczny
AND is_assembly_type = 0; --nie typ CLR
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • metadane typów zwraca widok sys.types (ale metadane typów tabelarycznych są też w sys.table_types),
  • typy też mają dwuczłonowe nazwy (znajdują się w schematach) – stąd użycie funkcji SCHEMA_NAME,
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi i apostrofami,
  • do uzyskania nazwy typu systemowego użytego na potrzeby danego typu użytkownika wykorzystałem funkcję TYPE_NAME wywołaną na identyfikatorze typu systemowego trzymanym w kolumnie system_type_id w widoku sys.types,
  • przy niektórych typach systemowych niezbędne jest podanie maksymalnej długości (typy binarne i tekstowe),  dokładności (data i czas), precyzji (float) albo precyzji i skali (decimal/numeric),
  • dzięki kolumnie is_nullable z widoku sys.types mogłem określić, czy typ pozwala domyślnie na przechowywanie wartości NULL, czy nie,
  • w klauzuli WHERE odfiltrowałem typy systemowe, typy tabelaryczne i typy CLR.

Słowo podsumowania

Skryptowanie typów prostych jest stosunkowo łatwe. W zasadzie wszystkie niezbędne metadane są do odczytania z jednego widoku systemowego (sys.types). A przy okazji można się choćby oswoić z niekoniecznie oczywistym faktem – typy też są trzymane w schematach. Skryptowanie pozostałych typów użytkownika (tabelarycznych i CLR) jest już nieco trudniejsze. Ale to temat na być może kolejny wpis na moim blogu :-)

[EDYCJA] Dziękuję Krzysiowi Stachyrze, który wypatrzył w tym wpisie czeski błąd – oczywiście SQL Server nie daje polecenia ALTER TYPE. ALTER TABLE na szczęście mamy :-) [/EDYCJA]

[PL] Skryptowanie w SQL Server 2008 – Obiekty proceduralne

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Pilot

Umiejętność korzystania z metadanych w celu skryptowania obiektów w bazach danych (tabel, procedur, itd.) przydaje się w życiu administratora i programisty baz danych. Sam wykorzystuję skryptowanie do cyklicznego tworzenia kopii kodu T-SQL obiektów w bazach danych (na wypadek, gdyby akurat okazało się, że w bazie uszkodzeniu uległ któryś z obiektów przechowujących metadane). Zapytasz, czemu nie użyć do tego mechanizmu skryptowania oferowanego przez Management Studio? Otóż skryptowanie z użyciem własnego kodu T-SQL daje mi więcej swobody, a ponadto skryptowanie w Management Studio ma swoje wady (patrz tutaj: [EN] SQL Server – Don’t (sp_)rename your database objects) i na dodatek często występują spore problemy w przypadku, gdy baza danych zawiera sporo obiektów (jakoś listowanie pokaźnej listy obiektów nie wychodzi narzędziu Management Studio – często aplikacja się zawiesza albo odpowiada wyjątkiem klasy OutOfMemoryException – patrz tutaj: Dziennik pokładowy MSSQL 2008 (wpis 0×0011) – Problem z Generate Script…).

Dlatego postanowiłem napocząć na blogu nową serię wpisów poświęconych tworzeniu własnego kodu T-SQL do skryptowania. Zaczynam od skryptowania obiektów proceduralnych – funkcji, procedur, widoków i wyzwalaczy. Potem spróbuję pokazać, jak można skryptować typy danych użytkownika, więzy, obiekty CLR i tabele. Tam, gdzie ilość kodu nie będzie wykraczała poza możliwości percepcji, będę się starał pokazać pełne rozwiązanie, ale na przykład w przypadku tabel już wiem, że nie będę podawał pełnego rozwiązania, a tylko podsunę kawałek kodu i parę pomysłów na dobry początek :-)

Kod będę tworzył z myślą o skryptowaniu w SQL Server 2008 i SQL Server 2008 R2. Przy odrobinie wysiłku kod będzie do zaadoptowania na potrzeby SQL Server 2005.

Funkcje, procedury, widoki

Zadanie: zeskryptować wszystkie funkcje, procedury i widoki w wybranej bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @schema_name sysname,
  @object_name sysname,
  @type_desc nvarchar(60),
  @type char(2),
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    SCHEMA_NAME(o.[schema_id]),
    o.name,
    o.type_desc COLLATE database_default,
    o.type COLLATE database_default,
    m.definition
  FROM sys.objects AS o
  INNER JOIN sys.sql_modules AS m
  ON m.[object_id] = o.[object_id]
  WHERE o.type IN ('FN', 'IF', 'P', 'TF', 'V')
  AND o.is_ms_shipped = 0
  ORDER BY SCHEMA_NAME(o.[schema_id]), o.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @schema_name,
  @object_name,
  @type_desc,
  @type,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    QUOTENAME(@schema_name) + '.' +
    QUOTENAME(@object_name), '''', ''''''
  ) + '''';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF OBJECT_ID(N''' +
      REPLACE(
        QUOTENAME(@schema_name) + '.' +
        QUOTENAME(@object_name), '''', ''''''
      ) + ''', N''' + (@type) + ''') IS NOT NULL';
    INSERT INTO @SQL (Line) SELECT '  DROP ' +
      CASE
        WHEN @type = 'P' THEN 'PROCEDURE'
        WHEN @type IN ('FN', 'IF', 'TF') THEN 'FUNCTION'
        ELSE 'VIEW'
      END + ' ' +
      QUOTENAME(@schema_name) + '.' +
      QUOTENAME(@object_name) + ';';

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @schema_name,
    @object_name,
    @type_desc,
    @type,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • definicje obiektów są trzymane w widoku sys.sql_modules w kolumnie definition, wyjątek stanowią obiekty z opcją WITH ENCRYPTION użytą w definicji (takie obiekty są “szyfrowane”),
  • listę obiektów stworzonych przez użytkowników w bazie danych zwraca widok sys.objects (ale jest też widok sys.all_objects, w którym dodatkowo są zawarte metadane obiektów systemowych),
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • zapytanie filtruje obiekty wg typu – wybiera funkcje trzech rodzajów, procedury i widoki, jest też filtr odrzucający ewentualne obiekty systemowe (kolumna is_ms_shipped w widoku sys.objects),
  • przed definicją każdego obiektu dorzucam kod wykrywający obecność obiektu w bazie danych i usuwający obiekt w przypadku, gdy takowy istnieje,
  • kolumny type i type_desc z widoku sys.objects mają na sztywno przypisane collation, stąd użycie funkcji COLLATE (inaczej nie dałoby się sklejać tych kolumn z tekstem, gdy collation bazy jest inne niż collation wspomnianych kolumn),
  • operuję zawsze dwuczłonowymi nazwami obiektów (dzięki temu nazwa jest jednoznaczna w kontekście bazy danych) – do uzyskania nazwy schematu obiektu wykorzystuję funkcję SCHEMA_NAME,
  • funkcja REPLACE jest przeze mnie użyta do “wyescape’owania” apostrofów w nazwach obiektów (swoją drogą, jak ktoś używa niestandardowych identyfikatorów zawierających dziwne znaczki i/lub słowa kluczowe, sam prosi się o kłopoty),
  • funkcja QUOTENAME pozwala otoczyć identyfikatory nawiasami kwadratowymi – zabezpieczam się przed niestandardowymi identyfikatorami.

Wyzwalacze na poziomie bazy

Zadanie: zeskryptować wszystkie wyzwalacze w wybranej bazie danych.

Rozwiązanie:

-- wyzwalacze DML i DDL (ON DATABASE)
USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @schema_name sysname,
  @object_name sysname,
  @type_desc nvarchar(60),
  @parent_class int,
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    OBJECT_SCHEMA_NAME(t.[object_id]),
    t.name,
    t.type_desc COLLATE database_default,
    t.parent_class,
    m.definition
  FROM sys.triggers AS t
  INNER JOIN sys.sql_modules AS m
  ON m.[object_id] = t.[object_id]
  WHERE t.is_ms_shipped = 0
  ORDER BY OBJECT_SCHEMA_NAME(t.[object_id]), t.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @schema_name,
  @object_name,
  @type_desc,
  @parent_class,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    ISNULL(QUOTENAME(@schema_name) + '.', '') +
    QUOTENAME(@object_name), '''', ''''''
  ) + ''';';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF EXISTS (SELECT * FROM sys.triggers WHERE name = N''' +
      REPLACE(
        ISNULL(QUOTENAME(@schema_name) + '.', '') +
        QUOTENAME(@object_name),
        '''',
        ''''''
      ) + ''')';
    INSERT INTO @SQL (Line)
    SELECT '  DROP TRIGGER ' +
      ISNULL(QUOTENAME(@schema_name) + '.', '') +
      QUOTENAME(@object_name) +
      CASE WHEN @parent_class = 0 THEN ' ON DATABASE' ELSE '' END + ';';

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @schema_name,
    @object_name,
    @type_desc,
    @parent_class,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • listę wyzwalaczy w bazie danych zwraca widok sys.triggers,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • drobnym wyzwaniem było rozróżnienie wyzwalaczy DML i DDL – każdy z tych typów wyzwalaczy wymaga nieco innego kodu przy usuwaniu, posłużyłem się kolumna parent_class z widoku sys.triggers,
  • ponieważ w widoku sys.triggers nie ma kolumny schema_id, musiałem użyć funkcji OBJECT_SCHEMA_NAME do wyłuskania nazwy schematu,
  • przy okazji taka obserwacja – wyzwalacz DML można utworzyć tylko w tym samym schemacie, w którym znajduje się nadrzędna tabela/widok.

Wyzwalacze na poziomie serwera

Zadanie: zeskryptować wszystkie wyzwalacze DDL na poziomie serwera.

Rozwiązanie:

USE master;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @object_name sysname,
  @type_desc nvarchar(60),
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    t.name,
    t.type_desc COLLATE database_default,
    m.definition
  FROM sys.server_triggers AS t
  INNER JOIN sys.server_sql_modules AS m
  ON m.[object_id] = t.[object_id]
  WHERE t.is_ms_shipped = 0
  ORDER BY t.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @object_name,
  @type_desc,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    QUOTENAME(@object_name), '''', ''''''
  ) + ''';';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF EXISTS (SELECT * FROM sys.server_triggers WHERE name = N''' +
      REPLACE(
        @object_name, '''', ''''''
      ) + ''')';
    INSERT INTO @SQL (Line)
    SELECT '  DROP TRIGGER ' +
      QUOTENAME(@object_name) + ' ON ALL SERVER;'

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @object_name,
    @type_desc,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • listę wyzwalaczy na poziomie serwera zwraca widok sys.server_triggers,
  • definicje widoków na poziomie serwera znajdują się w kolumnie definition widoku sys.server_sql_modules,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • tu było bodaj najłatwiej, ponieważ wyzwalacze na poziomie serwera nie mają przypisanego schematu.

Słowo podsumowania

Skryptowanie pojedynczego obiektu proceduralnego to nie problem. SQL Server oferuje procedurę sp_helptext oraz funkcję OBJECT_DEFINITION. Ale jeżeli chcemy hurtowo skryptować obiekty, podany w powyżej kod może okazać się przydatny. Można na jego podstawie zbudować własne procedury do skryptowania i używać ich na codzień. Tu polecam wpis z mojego starego bloga:  [PL][util] Piszemy narzędzia w T-SQL cz. 1 – sp_getcolumns.

Zachęcam do zabawy z metadanymi SQL Servera. Można się dzięki takiej zabawie sporo nauczyć. I oczywiście zachęcam do komentowania tego wpisu. Może masz jakieś uwagi, coś nie gra w podanym tu kodzie albo masz lepszy pomysł na realizację takich zadań.

[EDYCJA] To już druga wersja tego wpisu. W pierwszej kod T-SQL nie bazował na kursorach, a definicja każdego obiektu była zwracana w postaci jednej wartości typu nvarchar(max), co powodowało problem w przypadku, gdy definicja obiektu była dłuższa niż 8192 znaki (a nawet mniej, gdyż Management Studio domyślnie wyświetla w kolumnie bodajże do 255 znaków). Druga wersja powstała dzięki czujności (po raz kolejny) Krzysia Stachyry. Krzysiu, wielki szacunek za cierpliwe testowanie moich wypocin! Tym razem powinno być ciut lepiej :-) [/EDYCJA]