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

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

_selectFirst

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

आर्गुमेंट:

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

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

mutation फ़ील्ड

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

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

आर्गुमेंट:

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

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

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

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

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

_executeReturning

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

आर्गुमेंट:

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

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

_executeReturningFirst

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

आर्गुमेंट:

  • sql: 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) डायरेक्टिव का इस्तेमाल करके, Postgres आइडेंटिफ़ायर को साफ़ तौर पर पसंद के मुताबिक बनाया जा सकता है.

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

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.movies का इस्तेमाल किया जा सकता है.data._select

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 और Auth कॉन्टेक्स्ट के साथ 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, डेटा में बदलाव करने वाले सीटीई को डेटा में बदलाव करने वाले दूसरे स्टेटमेंट में नेस्ट करने की अनुमति नहीं देता. इससे क्वेरी में गड़बड़ी होती है.

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

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

इस उदाहरण में, मान लें कि आपके रेस्टोरेंट ऐप्लिकेशन में एक टेबल है, जो मेटाडेटा JSON कॉलम में जगह का डेटा सेव करती है. जैसे, {"latitude": 37.3688, "longitude": -122.0363}. अगर आपने PostGIS एक्सटेंशन चालू किया है, तो Postgres 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 इंजेक्शन से पूरी तरह सुरक्षित रखा जा सकता है. हालांकि, अगर SQL का इस्तेमाल करके, Postgres की सेव की गई कस्टम प्रक्रियाओं या डाइनैमिक 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;
$$;