नेटिव एसक्यूएल का इस्तेमाल करके, Firebase SQL Connect की कार्रवाइयां लागू करना

GraphQL के बजाय SQL का इस्तेमाल करके, Firebase SQL Connect के ऑपरेशन लिखने के बारे में जानकारी. page_type: guide announcement: > Native SQL, सुविधा की झलक के तौर पर उपलब्ध है. इसका मतलब है कि यह किसी भी एसएलए या समर्थन नहीं होने की नीति के तहत नहीं आता. साथ ही, इसमें बैकवर्ड-इनकंपैटिबल तरीके से बदलाव किया जा सकता है. अगर इस सुविधा का इस्तेमाल, सेव की गई उन प्रक्रियाओं या फ़ंक्शन के साथ किया जाता है जो डाइनैमिक SQL को लागू करते हैं, तो इस पेज पर सबसे नीचे बताए गए सुरक्षा के सबसे सही तरीकों को अपनाएं.

Firebase SQL Connect आपके Cloud SQL डेटाबेस के साथ इंटरैक्ट करने के कई तरीके उपलब्ध कराता है:

  • Native GraphQL: अपने schema.gql में टाइप तय करें. इसके बाद, SQL Connect GraphQL के ऑपरेशन को SQL में बदलता है. यह स्टैंडर्ड तरीका है. इसमें टाइपिंग और स्कीमा-एनफ़ोर्स्ड स्ट्रक्चर की सुविधा मिलती है. इस पेज के अलावा, SQL Connect के ज़्यादातर दस्तावेज़ों में इस विकल्प के बारे में बताया गया है. अगर मुमकिन हो, तो इस तरीके का इस्तेमाल करें, ताकि टाइप की पूरी सुरक्षा और टूलिंग की सुविधा का फ़ायदा मिल सके.
  • The @view डायरेक्टिव: कस्टम SELECT एसक्यूएल स्टेटमेंट के आधार पर, schema.gql में GraphQL टाइप तय करें. यह, जटिल SQL लॉजिक के आधार पर, सिर्फ़ पढ़ने के लिए, मज़बूत टाइप वाले व्यू बनाने के लिए काम का है. इन टाइप के लिए, सामान्य टाइप की तरह क्वेरी की जा सकती है. @view देखें.
  • Native SQL: खास रूट फ़ील्ड का इस्तेमाल करके, .gql फ़ाइलों में नाम वाले ऑपरेशन में सीधे तौर पर SQL स्टेटमेंट एम्बेड करें. इससे ज़्यादा फ़्लेक्सिबिलिटी और डायरेक्ट कंट्रोल मिलता है. खास तौर पर, उन ऑपरेशन के लिए जो स्टैंडर्ड GraphQL के साथ काम नहीं करते. साथ ही, डेटाबेस की खास सुविधाओं का इस्तेमाल करने या PostgreSQL एक्सटेंशन का इस्तेमाल करने के लिए भी यह तरीका काम का है. GraphQL और @view डायरेक्टिव के उलट, native SQL से मज़बूत टाइप वाला आउटपुट नहीं मिलता.

इस गाइड में, Native SQL विकल्प पर फ़ोकस किया गया है.

Native SQL के इस्तेमाल के सामान्य उदाहरण

Native GraphQL से टाइप की पूरी सुरक्षा मिलती है. वहीं, @view डायरेक्टिव से, सिर्फ़ पढ़ने के लिए SQL रिपोर्ट के लिए मज़बूत टाइप वाले नतीजे मिलते हैं. इसके अलावा, native SQL से ये काम किए जा सकते हैं:

  • PostgreSQL एक्सटेंशन: अपने GraphQL स्कीमा में जटिल टाइप को मैप किए बिना, इंस्टॉल किए गए किसी भी PostgreSQL एक्सटेंशन (जैसे, जियोस्पेशल डेटा के लिए PostGIS) के लिए सीधे तौर पर क्वेरी करें और उसका इस्तेमाल करें.
  • जटिल क्वेरी: जॉइन, सबक्वेरी, एग्रीगेशन, विंडो फ़ंक्शन, और सेव की गई प्रक्रियाओं के साथ जटिल SQL लागू करें.
  • डेटा मैनिप्युलेशन (डीएमएल): INSERT, UPDATE, DELETE ऑपरेशन सीधे तौर पर लागू करें. (हालांकि, डेटा डेफ़िनिशन लैंग्वेज (डीडीएल) कमांड के लिए, native SQL का इस्तेमाल न करें. बैकएंड और जनरेट किए गए एसडीके को सिंक में रखने के लिए, आपको GraphQL का इस्तेमाल करके स्कीमा-लेवल में बदलाव करने होंगे.)
  • डेटाबेस की खास सुविधाएं: PostgreSQL के लिए खास तौर पर बने फ़ंक्शन, ऑपरेटर या डेटा टाइप का इस्तेमाल करें.
  • परफ़ॉर्मेंस ऑप्टिमाइज़ेशन: अहम पाथ के लिए, SQL स्टेटमेंट को मैन्युअल तरीके से ट्यून करें.

Native SQL के रूट फ़ील्ड

SQL का इस्तेमाल करके ऑपरेशन लिखने के लिए, query या mutation टाइप के इनमें से किसी एक रूट फ़ील्ड का इस्तेमाल करें:

query फ़ील्ड

फ़ील्ड ब्यौरा
_select

एक SQL क्वेरी लागू करता है, जिससे कोई भी लाइन या एक से ज़्यादा लाइनें दिखती हैं.

आर्गुमेंट:

  • sql: एसक्यूएल स्टेटमेंट स्ट्रिंग लिटरल. SQL इंजेक्शन से बचने के लिए, पैरामीटर वैल्यू के लिए पोज़िशनल प्लेसहोल्डर ($1,$2, वगैरह) का इस्तेमाल करें.
  • params: प्लेसहोल्डर से बाइंड करने के लिए, वैल्यू की क्रम वाली सूची. इसमें लिटरल, GraphQL वैरिएबल, और सर्वर-इंजेक्ट किए गए खास कॉन्टेक्स्ट मैप शामिल हो सकते हैं. जैसे,{_expr: "auth.uid"} (पुष्टि किए गए उपयोगकर्ता का आईडी).

नतीजा: JSON अरे ([Any]).

_selectFirst

एक SQL क्वेरी लागू करता है, जिससे कोई भी लाइन या एक लाइन दिखती है.

आर्गुमेंट:

  • sql: एसक्यूएल स्टेटमेंट स्ट्रिंग लिटरल. SQL इंजेक्शन से बचने के लिए, पैरामीटर वैल्यू के लिए पोज़िशनल प्लेसहोल्डर ($1,$2, वगैरह) का इस्तेमाल करें.
  • params: प्लेसहोल्डर से बाइंड करने के लिए, वैल्यू की क्रम वाली सूची. इसमें लिटरल, GraphQL वैरिएबल, और सर्वर-इंजेक्ट किए गए खास कॉन्टेक्स्ट मैप शामिल हो सकते हैं. जैसे,{_expr: "auth.uid"} (पुष्टि किए गए उपयोगकर्ता का आईडी).

नतीजा: JSON ऑब्जेक्ट (Any) या null.

mutation फ़ील्ड

फ़ील्ड ब्यौरा
_execute

डीएमएल स्टेटमेंट (INSERT, UPDATE, DELETE) लागू करता है.

आर्गुमेंट:

  • sql: एसक्यूएल स्टेटमेंट स्ट्रिंग लिटरल. SQL इंजेक्शन से बचने के लिए, पैरामीटर वैल्यू के लिए पोज़िशनल प्लेसहोल्डर ($1,$2, वगैरह) का इस्तेमाल करें.

    यहां डेटा में बदलाव करने वाले कॉमन टेबल एक्सप्रेशन (उदाहरण के लिए, WITH new_row AS (INSERT...)) का इस्तेमाल किया जा सकता है, क्योंकि यह फ़ील्ड सिर्फ़ लाइन की संख्या दिखाता है. सिर्फ़ _execute में सीटीई इस्तेमाल किए जा सकते हैं.

  • params: प्लेसहोल्डर से बाइंड करने के लिए, वैल्यू की क्रम वाली सूची. इसमें लिटरल, GraphQL वैरिएबल, और सर्वर-इंजेक्ट किए गए खास कॉन्टेक्स्ट मैप शामिल हो सकते हैं. जैसे,{_expr: "auth.uid"} (पुष्टि किए गए उपयोगकर्ता का आईडी).

नतीजा: Int (बदली गई लाइनों की संख्या ).

नतीजे में, RETURNING क्लॉज़ को अनदेखा किया जाता है.

_executeReturning

RETURNING क्लॉज़ के साथ डीएमएल स्टेटमेंट लागू करता है, जिससे कोई भी लाइन या एक से ज़्यादा लाइनें दिखती हैं.

आर्गुमेंट:

  • sql: एसक्यूएल स्टेटमेंट स्ट्रिंग लिटरल. SQL इंजेक्शन से बचने के लिए, पैरामीटर वैल्यू के लिए पोज़िशनल प्लेसहोल्डर ($1,$2, वगैरह) का इस्तेमाल करें. डेटा में बदलाव करने वाले कॉमन टेबल एक्सप्रेशन इस्तेमाल नहीं किए जा सकते.
  • params: प्लेसहोल्डर से बाइंड करने के लिए, वैल्यू की क्रम वाली सूची. इसमें लिटरल, GraphQL वैरिएबल, और सर्वर-इंजेक्ट किए गए खास कॉन्टेक्स्ट मैप शामिल हो सकते हैं. जैसे,{_expr: "auth.uid"} (पुष्टि किए गए उपयोगकर्ता का आईडी).

नतीजा: JSON अरे ([Any]).

_executeReturningFirst

RETURNING क्लॉज़ के साथ डीएमएल स्टेटमेंट लागू करता है, जिससे कोई भी लाइन या एक लाइन दिखती है.

आर्गुमेंट:

  • sql: एसक्यूएल स्टेटमेंट स्ट्रिंग लिटरल. SQL इंजेक्शन से बचने के लिए, पैरामीटर वैल्यू के लिए पोज़िशनल प्लेसहोल्डर ($1,$2, वगैरह) का इस्तेमाल करें. डेटा में बदलाव करने वाले कॉमन टेबल एक्सप्रेशन इस्तेमाल नहीं किए जा सकते.
  • params: प्लेसहोल्डर से बाइंड करने के लिए, वैल्यू की क्रम वाली सूची. इसमें लिटरल, GraphQL वैरिएबल, और सर्वर-इंजेक्ट किए गए खास कॉन्टेक्स्ट मैप शामिल हो सकते हैं. जैसे,{_expr: "auth.uid"} (पुष्टि किए गए उपयोगकर्ता का आईडी).

नतीजा: JSON ऑब्जेक्ट (Any) या null.

ध्यान दें:

  • ऑपरेशन, SQL Connect सेवा खाते को दी गई अनुमतियों का इस्तेमाल करके लागू किए जाते हैं.

सिंटैक्स के नियम और सीमाएं

Native SQL, पार्स करने के सख्त नियमों को लागू करता है, ताकि सुरक्षा पक्की की जा सके और SQL इंजेक्शन को रोका जा सके. यहां बताई गई बातों का ध्यान रखें:

  • टिप्पणियां: ब्लॉक टिप्पणियों (/* ... */) का इस्तेमाल करें. लाइन टिप्पणियों (--) का इस्तेमाल नहीं किया जा सकता, क्योंकि ये क्वेरी को जोड़ने के दौरान, बाद के क्लॉज़ (जैसे, सुरक्षा फ़िल्टर) को छोटा कर सकती हैं.
  • पैरामीटर: पोज़िशनल पैरामीटर ($1, $2) का इस्तेमाल करें. ये params अरे के क्रम से मेल खाते हैं. नाम वाले पैरामीटर ($id, :name) मौजूद नहीं हैं.
  • स्ट्रिंग: एक्सटेंडेड स्ट्रिंग लिटरल (E'...') और डॉलर-कोटेड स्ट्रिंग ($$...$$) इस्तेमाल किए जा सकते हैं. PostgreSQL Unicode एस्केप (U&'...') इस्तेमाल नहीं किए जा सकते.

टिप्पणियों में पैरामीटर

पार्सर, ब्लॉक टिप्पणी में मौजूद हर चीज़ को अनदेखा करता है. अगर आपने किसी पैरामीटर वाली लाइन पर टिप्पणी की है (उदाहरण के लिए, /* WHERE id = $1 */), तो आपको params सूची से उस पैरामीटर को भी हटाना होगा. ऐसा न करने पर, ऑपरेशन में गड़बड़ी होगी और unused parameter: $1 मैसेज दिखेगा.

नेमिंग कनवेंशन

Native SQL लिखते समय, सीधे तौर पर PostgreSQL डेटाबेस के साथ इंटरैक्ट किया जाता है. इसलिए, टेबल और कॉलम के लिए डेटाबेस के असली नामों का इस्तेमाल करना ज़रूरी है. डिफ़ॉल्ट रूप से, SQL Connect GraphQL स्कीमा में मौजूद नामों को डेटाबेस में स्नेक केस में अपने-आप मैप करता है. हालांकि, @table(name) और @col(name) डायरेक्टिव का इस्तेमाल करके, PostgreSQL आइडेंटिफ़ायर को साफ़ तौर पर पसंद के मुताबिक बनाया जा सकता है.

अगर डायरेक्टिव के बिना कोई टाइप तय किया जाता है, तो GraphQL टेबल और फ़ील्ड के नाम, डिफ़ॉल्ट snake_case PostgreSQL आइडेंटिफ़ायर पर मैप होते हैं:

schema.gql queries.gql
type UserProfile {
  userId: ID!
  displayName: String
}
query GetUserProfileDefault($id: ID!) {
  profile: _selectFirst(
    sql: """
      SELECT user_id, display_name
      FROM user_profile
      WHERE user_id = $1
    """,
    params: [$id]
  )
}

डिफ़ॉल्ट रूप से, PostgreSQL आइडेंटिफ़ायर केस-इनसेंसिटिव होते हैं. अगर @table या @col जैसे डायरेक्टिव का इस्तेमाल करके, ऐसा नाम तय किया जाता है जिसमें अपरकेस या मिक्स-केस लेटर शामिल हैं, तो आपको SQL स्टेटमेंट में उस आइडेंटिफ़ायर को डबल कोट में रखना होगा.

यहां दिए गए उदाहरण में, टेबल के नाम के लिए "UserProfiles" और "profileId" के लिए userId का इस्तेमाल करना होगा. displayName फ़ील्ड, डिफ़ॉल्ट रूप से display_name में बदल जाता है:

schema.gql queries.gql
type UserProfileCustom @table(name: "UserProfiles") {
  userId: ID! @col(name: "profileId")
  displayName: String
}
query GetUserProfileCustom($id: ID!) {
  profile: _selectFirst(
    sql: """
      SELECT "profileId", display_name
      FROM "UserProfiles"
      WHERE "profileId" = $1
    """,
    params: [$id]
  )
}

इस्तेमाल करने के उदाहरण

पहला उदाहरण: फ़ील्ड के लिए एलियासिंग के साथ सामान्य SELECT

क्लाइंट रिस्पॉन्स को साफ़-सुथरा बनाने के लिए, रूट फ़ील्ड के लिए एलियास बनाया जा सकता है. जैसे, movies: _select. इससे data._select के बजाय data.movies दिखेगा.

queries.gql:

query GetMoviesByGenre($genre: String!, $limit: Int!) @auth(level: PUBLIC) {
  movies: _select(
    sql: """
      SELECT id, title, release_year, rating
      FROM movie
      WHERE genre = $1
      ORDER BY release_year DESC
      LIMIT $2
    """,
    params: [$genre, $limit]
  )
}

क्लाइंट एसडीके का इस्तेमाल करके क्वेरी चलाने के बाद, नतीजा data.movies में दिखेगा.

दूसरा उदाहरण: सामान्य UPDATE

mutations.gql:

mutation UpdateMovieRating(
  $movieId: UUID!,
  $newRating: Float!
) @auth(level: NO_ACCESS) {
  _execute(
    sql: """
      UPDATE movie
      SET rating = $2
      WHERE id = $1
    """,
    params: [$movieId, $newRating]
  )
}

क्लाइंट एसडीके का इस्तेमाल करके म्यूटेशन चलाने के बाद, बदली गई लाइनों की संख्या data._execute में दिखेगी.

तीसरा उदाहरण: सामान्य एग्रीगेशन

queries.gql:

query GetTotalReviewCount @auth(level: PUBLIC) {
  stats: _selectFirst(
    sql: "SELECT COUNT(*) as total_reviews FROM \"Reviews\""
  )
}

क्लाइंट एसडीके का इस्तेमाल करके क्वेरी चलाने के बाद, नतीजा data.stats.total_reviews में दिखेगा.

चौथा उदाहरण: RANK के साथ अडवांस एग्रीगेशन

queries.gql:

query GetMoviesRankedByRating @auth(level: PUBLIC) {
  _select(
    sql: """
      SELECT
        id,
        title,
        rating,
        RANK() OVER (ORDER BY rating DESC) as rank
      FROM movie
      WHERE rating IS NOT NULL
      LIMIT 20
    """,
    params: []
  )
}

क्लाइंट एसडीके का इस्तेमाल करके क्वेरी चलाने के बाद, नतीजा data._select में दिखेगा.

पांचवा उदाहरण: RETURNING और पुष्टि करने के कॉन्टेक्स्ट के साथ UPDATE

mutations.gql:

mutation UpdateMyReviewText(
  $movieId: UUID!,
  $newText: String!
) @auth(level: USER) {
  updatedReview: _executeReturningFirst(
    sql: """
      UPDATE "Reviews"
      SET review_text = $2
      WHERE movie_id = $1 AND user_id = $3
      RETURNING movie_id, user_id, rating, review_text
    """,
    params: [$movieId, $newText, {_expr: "auth.uid"}]
  )
}

क्लाइंट एसडीके का इस्तेमाल करके म्यूटेशन चलाने के बाद, अपडेट किया गया पोस्ट डेटा data.updatedReview में दिखेगा.

छठा उदाहरण: अपसर्ट (एटॉमिक गेट-ऑर-क्रिएट) के साथ अडवांस सीटीई

इस पैटर्न का इस्तेमाल, डिपेंडेंट रिकॉर्ड (जैसे, उपयोगकर्ता या फ़िल्में) मौजूद हैं या नहीं, यह पक्का करने के लिए किया जाता है. इसके बाद, चाइल्ड रिकॉर्ड (जैसे, समीक्षा) को सिंगल डेटाबेस ट्रांज़ैक्शन में डाला जाता है.

mutations.gql:

mutation CreateMovieCTE($movieId: UUID!, $userId: UUID!, $reviewId: UUID!) @auth(level: USER) {
  _execute(
    sql: """
      WITH
      new_user AS (
        INSERT INTO "user" (id, username)
        VALUES ($2, 'Auto-Generated User')
        ON CONFLICT (id) DO NOTHING
        RETURNING id
      ),
      movie AS (
        INSERT INTO movie (id, title, image_url, release_year, genre)
        VALUES ($1, 'Auto-Generated Movie', 'https://placeholder.com', 2025, 'Sci-Fi')
        ON CONFLICT (id) DO NOTHING
        RETURNING id
      )
      INSERT INTO "Reviews" (id, movie_id, user_id, rating, review_text, review_date)
      VALUES (
        $3,
        $1,
        $2,
        5,
        'Good!',
        NOW()
      )
    """,
    params: [$movieId, $userId, $reviewId]
  )
}

_executeReturning और _executeReturningFirst, आपकी क्वेरी को पैरंट सीटीई में रैप करते हैं, ताकि आउटपुट को JSON के तौर पर फ़ॉर्मैट किया जा सके. PostgreSQL, डेटा में बदलाव करने वाले सीटीई को डेटा में बदलाव करने वाले दूसरे स्टेटमेंट में नेस्ट करने की अनुमति नहीं देता. इस वजह से, क्वेरी में गड़बड़ी होती है.

सातवां उदाहरण: PostgreSQL एक्सटेंशन का इस्तेमाल करना

Native SQL की मदद से, PostgreSQL एक्सटेंशन का इस्तेमाल किया जा सकता है. जैसे, PostGIS. इसके लिए, आपको अपने GraphQL स्कीमा में जटिल जियोमेट्री टाइप को मैप करने या अपनी टेबल में बदलाव करने की ज़रूरत नहीं होती.

इस उदाहरण में, मान लें कि आपके रेस्टोरेंट ऐप्लिकेशन में एक टेबल है. इसमें, मेटाडेटा JSON कॉलम में जगह का डेटा सेव किया जाता है. जैसे, {"latitude": 37.3688, "longitude": -122.0363}. अगर आपने PostGIS एक्सटेंशन चालू किया है, तो स्टैंडर्ड PostgreSQL JSON ऑपरेटर (->>) का इस्तेमाल करके, इन वैल्यू को तुरंत निकाला जा सकता है. इसके बाद, इन्हें PostGIS ST_MakePoint फ़ंक्शन में पास किया जा सकता है.

query GetNearbyActiveRestaurants(
  $userLong: Float!,
  $userLat: Float!,
  $maxDistanceMeters: Float!
) @auth(level: USER) {
  nearby: _select(
    sql: """
      SELECT
        id,
        name,
        tags,
        ST_Distance(
          ST_MakePoint(
            (metadata->>'longitude')::float,
            (metadata->>'latitude')::float
          )::geography,
          ST_MakePoint($1, $2)::geography
        ) as distance_meters
      FROM restaurant
      WHERE active = true
        AND metadata ? 'longitude' AND metadata ? 'latitude'
        AND ST_DWithin(
          ST_MakePoint(
            (metadata->>'longitude')::float,
            (metadata->>'latitude')::float
          )::geography,
          ST_MakePoint($1, $2)::geography,
          $3
        )
      ORDER BY distance_meters ASC
      LIMIT 10
    """,
    params: [$userLong, $userLat, $maxDistanceMeters]
  )
}

क्लाइंट एसडीके का इस्तेमाल करके क्वेरी चलाने के बाद, नतीजा data.nearby में दिखेगा.

सुरक्षा के सबसे सही तरीके: डाइनैमिक SQL और सेव की गई प्रक्रियाएं

SQL Connect GraphQL-से-डेटाबेस बाउंड्री पर सभी इनपुट को सुरक्षित तरीके से पैरामीटर में बदलता है. इससे, स्टैंडर्ड एसक्यूएल क्वेरी को पहले ऑर्डर के SQL इंजेक्शन से पूरी तरह सुरक्षित रखा जा सकता है. हालांकि, अगर SQL का इस्तेमाल करके, PostgreSQL की सेव की गई कस्टम प्रक्रियाओं या उन फ़ंक्शन को कॉल किया जाता है जो डाइनैमिक SQL को लागू करते हैं, तो आपको यह पक्का करना होगा कि आपका इंटरनल PL/pgSQL कोड, इन पैरामीटर को सुरक्षित तरीके से हैंडल करे.

अगर सेव की गई आपकी प्रक्रिया, उपयोगकर्ता के इनपुट को सीधे तौर पर EXECUTE स्ट्रिंग में जोड़ती है, तो यह पैरामीटर में बदलने की प्रोसेस को बायपास कर देती है. इससे, दूसरे ऑर्डर के SQL इंजेक्शन की समस्या हो सकती है:

-- INSECURE: Do not concatenate parameters into dynamic strings!
CREATE OR REPLACE PROCEDURE unsafe_update(user_input TEXT)
LANGUAGE plpgsql AS $$
BEGIN
    -- A malicious user_input (e.g., "val'; DROP TABLE users; --")
    -- will execute as code.
    EXECUTE 'UPDATE target_table SET status = ''' || user_input || '''';
END;
$$;

इससे बचने के लिए, इन सबसे सही तरीकों को अपनाएं:

  • USING क्लॉज़ का इस्तेमाल करें: सेव की गई अपनी प्रक्रियाओं में डाइनैमिक SQL लिखते समय, डेटा पैरामीटर को सुरक्षित तरीके से बाइंड करने के लिए हमेशा USING क्लॉज़ का इस्तेमाल करें.
  • आइडेंटिफ़ायर के लिए format() का इस्तेमाल करें: सुरक्षित डेटाबेस आइडेंटिफ़ायर इंजेक्शन (जैसे, टेबल के नाम) के लिए, %I फ़्लैग के साथ format() का इस्तेमाल करें.
  • आइडेंटिफ़ायर को सख्ती से अनुमति दें: क्लाइंट ऐप्लिकेशन को डेटाबेस आइडेंटिफ़ायर चुनने की अनुमति न दें. अगर आपकी प्रक्रिया के लिए डाइनैमिक आइडेंटिफ़ायर की ज़रूरत है, तो लागू करने से पहले, अपने PL/pgSQL लॉजिक में हार्डकोड की गई अनुमति वाली सूची के हिसाब से इनपुट की पुष्टि करें.
-- SECURE: Use format() for identifiers and USING for data values
CREATE OR REPLACE PROCEDURE secure_update(
    target_table TEXT, new_value TEXT, row_id INT
)
LANGUAGE plpgsql AS $$
BEGIN
    -- Validate the dynamic table name against an allowlist
    IF target_table NOT IN ('orders', 'users', 'inventory') THEN
        RAISE EXCEPTION 'Invalid table name';
    END IF;

    -- Execute securely
    EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table)
    USING new_value, row_id;
END;
$$;