diff --git a/pypdf/_doc_common.py b/pypdf/_doc_common.py
index 84e99208a..0baab4fc7 100644
--- a/pypdf/_doc_common.py
+++ b/pypdf/_doc_common.py
@@ -492,17 +492,19 @@ def get_fields(
         tree: Optional[TreeObject] = None,
         retval: Optional[Dict[Any, Any]] = None,
         fileobj: Optional[Any] = None,
+        stack: Optional[List[PdfObject]] = None,
     ) -> Optional[Dict[str, Any]]:
         """
         Extract field data if this PDF contains interactive form fields.

-        The *tree* and *retval* parameters are for recursive use.
+        The *tree*, *retval*, *stack* parameters are for recursive use.

         Args:
-            tree:
-            retval:
+            tree: Current object to parse.
+            retval: In-progress list of fields.
             fileobj: A file object (usually a text file) to write
                 a report to on all interactive form fields found.
+            stack: List of already parsed objects.

         Returns:
             A dictionary where each key is a field name, and each
@@ -515,6 +517,7 @@ def get_fields(
         if retval is None:
             retval = {}
             catalog = self.root_object
+            stack = []
             # get the AcroForm tree
             if CD.ACRO_FORM in catalog:
                 tree = cast(Optional[TreeObject], catalog[CD.ACRO_FORM])
@@ -522,19 +525,15 @@ def get_fields(
                 return None
         if tree is None:
             return retval
-        self._check_kids(tree, retval, fileobj)
-        for attr in field_attributes:
-            if attr in tree:
-                # Tree is a field
-                self._build_field(tree, retval, fileobj, field_attributes)
-                break
-
+        assert stack is not None
         if "/Fields" in tree:
             fields = cast(ArrayObject, tree["/Fields"])
             for f in fields:
                 field = f.get_object()
-                self._build_field(field, retval, fileobj, field_attributes)
-
+                self._build_field(field, retval, fileobj, field_attributes, stack)
+        elif any(attr in tree for attr in field_attributes):
+            # Tree is a field
+            self._build_field(tree, retval, fileobj, field_attributes, stack)
         return retval

     def _get_qualified_field_name(self, parent: DictionaryObject) -> str:
@@ -557,25 +556,11 @@ def _build_field(
         retval: Dict[Any, Any],
         fileobj: Any,
         field_attributes: Any,
+        stack: List[PdfObject],
     ) -> None:
-        self._check_kids(field, retval, fileobj)
-        try:
-            key = cast(str, field["/TM"])
-        except KeyError:
-            try:
-                if "/Parent" in field:
-                    key = (
-                        self._get_qualified_field_name(
-                            cast(DictionaryObject, field["/Parent"])
-                        )
-                        + "."
-                    )
-                else:
-                    key = ""
-                key += cast(str, field["/T"])
-            except KeyError:
-                # Ignore no-name field for now
-                return
+        if all(attr not in field for attr in ("/T", "/TM")):
+            return
+        key = self._get_qualified_field_name(field)
         if fileobj:
             self._write_field(fileobj, field, field_attributes)
             fileobj.write("\n")
@@ -604,14 +589,27 @@ def _build_field(
                 and "/Off" in retval[key]["/_States_"]
             ):
                 del retval[key]["/_States_"][retval[key]["/_States_"].index("/Off")]
+        # at last for order
+        self._check_kids(field, retval, fileobj, stack)

     def _check_kids(
-        self, tree: Union[TreeObject, DictionaryObject], retval: Any, fileobj: Any
+        self,
+        tree: Union[TreeObject, DictionaryObject],
+        retval: Any,
+        fileobj: Any,
+        stack: List[PdfObject],
     ) -> None:
+        if tree in stack:
+            logger_warning(
+                f"{self._get_qualified_field_name(tree)} already parsed", __name__
+            )
+            return
+        stack.append(tree)
         if PA.KIDS in tree:
             # recurse down the tree
             for kid in tree[PA.KIDS]:  # type: ignore
-                self.get_fields(kid.get_object(), retval, fileobj)
+                kid = kid.get_object()
+                self.get_fields(kid, retval, fileobj, stack)

     def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None:
         field_attributes_tuple = FA.attributes()

diff --git a/tests/test_reader.py b/tests/test_reader.py
index 83b61bc59..4557270bb 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -1530,3 +1530,30 @@ def test_damaged_pdf():
     assert (
         exc.value.args[0] == "Expected object ID (21 0) does not match actual (-1 -1)."
     )
+
+
+@pytest.mark.enable_socket()
+@pytest.mark.timeout(10)
+def test_looping_form(caplog):
+    """Cf iss 2643"""
+    url = "https://github.com/py-pdf/pypdf/files/15306053/inheritance.pdf"
+    name = "iss2643.pdf"
+    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)), strict=False)
+    flds = reader.get_fields()
+    assert all(
+        x in flds
+        for x in (
+            "Text10",
+            "Text10.0.0.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1",
+            "amt1.0",
+            "amt1.1",
+            "DSS#3pg3#0hgu7",
+        )
+    )
+    writer = PdfWriter(reader)
+    writer.root_object["/AcroForm"]["/Fields"][5]["/Kids"].append(
+        writer.root_object["/AcroForm"]["/Fields"][5]["/Kids"][0]
+    )
+    flds2 = writer.get_fields()
+    assert "Text68.0 already parsed" in caplog.text
+    assert list(flds.keys()) == list(flds2.keys())

ROB: reading PDF with multiple forms is really slow
When reading a specific PDF file with a lot of forms, the `get_fields()` seems to loop for a very long time (one hour and a half on my setup).

## Environment

Which environment were you using when you encountered the problem?

```bash
$ python -m platform
Linux-5.4.0-173-generic-x86_64-with-glibc2.29

$ python -c "import pypdf;print(pypdf._debug_versions)"
pypdf==4.2.0, crypt_provider=('pycryptodome', '3.20.0'), PIL=10.3.0
```

## Code + PDF

This is a minimal, complete example that shows the issue:

```python
from pypdf import PdfReader
with open('inheritance.pdf', 'rb') as f:
    doc = PdfReader(f)
    doc.get_fields()
```

The PDF file (coming from https://www.nj.gov/treasury/taxation/pdf/other_forms/inheritance/itrbk.pdf):
[inheritance.pdf](https://github.com/py-pdf/pypdf/files/15306053/inheritance.pdf)

## Traceback

The `get_fields()` function seems to make a lot of recursive `_check_kids()` calls

```
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1115, 0, 133206044573808), IndirectObject(1865, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1107, 0, 133206044573808), IndirectObject(1866, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1107, 0, 133206044573808), IndirectObject(1866, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1071, 0, 133206044573808), IndirectObject(1864, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1115, 0, 133206044573808), IndirectObject(1865, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1107, 0, 133206044573808), IndirectObject(1866, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1107, 0, 133206044573808), IndirectObject(1866, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1115, 0, 133206044573808), IndirectObject(1865, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1107, 0, 133206044573808), IndirectObject(1866, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(942, 0, 133206044573808), IndirectObject(922, 0, 133206044573808)]
Recursing down 5 kids: [IndirectObject(1120, 0, 133206044573808), IndirectObject(1867, 0, 133206044573808)]
```

Thanks for the report. That surely does not look right.
If ever useful: another (smaller) file where the same problem happens: https://www.courts.ca.gov/documents/fl410.pdf
@farjasju
You should have a look  at the document with PdfBox with debug function to look at the pdf structure. There is a lot of fields that exists in the document (fields can be identified in the field /T. You may see a lot of fields named 0, 1 : they are different from each others : the qualified name are differents.
There might be some optimisation. also can you check the document does not includes some (infinite)loops (where some protection may be required)
I confirm that the file is "corrupted" with some objects being a grand child of itself:

![67820872-daee-48e8-aa91-0c83441be86b](https://github.com/py-pdf/pypdf/assets/4083478/3aa8ccc1-34a7-45ad-accc-9bd8ba7d19d1)


diff --git a/pypdf/filters.py b/pypdf/filters.py
index d62cf7842..d573ae30e 100644
--- a/pypdf/filters.py
+++ b/pypdf/filters.py
@@ -80,14 +80,19 @@ def decompress(data: bytes) -> bytes:
     try:
         return zlib.decompress(data)
     except zlib.error:
-        d = zlib.decompressobj(zlib.MAX_WBITS | 32)
-        result_str = b""
-        for b in [data[i : i + 1] for i in range(len(data))]:
-            try:
-                result_str += d.decompress(b)
-            except zlib.error:
-                pass
-        return result_str
+        try:
+            # For larger files, use Decompress object to enable buffered reading
+            return zlib.decompressobj().decompress(data)
+        except zlib.error:
+            # If still failed, then try with increased window size
+            d = zlib.decompressobj(zlib.MAX_WBITS | 32)
+            result_str = b""
+            for b in [data[i : i + 1] for i in range(len(data))]:
+                try:
+                    result_str += d.decompress(b)
+                except zlib.error:
+                    pass
+            return result_str

 class FlateDecode:

diff --git a/tests/bench.py b/tests/bench.py
index dcfc30a9b..ea5597f88 100644
--- a/tests/bench.py
+++ b/tests/bench.py
@@ -227,3 +227,15 @@ def test_image_new_property_performance(benchmark):
     data = BytesIO(get_data_from_url(url, name=name))

     benchmark(image_new_property, data)
+
+
+def image_extraction(data):
+    reader = PdfReader(data)
+    list(reader.pages[0].images)
+
+
+@pytest.mark.enable_socket()
+def test_large_compressed_image_performance(benchmark):
+    url = "https://github.com/py-pdf/pypdf/files/15306199/file_with_large_compressed_image.pdf"
+    data = BytesIO(get_data_from_url(url, name="file_with_large_compressed_image.pdf"))
+    benchmark(image_extraction, data)
diff --git a/tests/test_images.py b/tests/test_images.py
index ad694d669..e77090171 100644
--- a/tests/test_images.py
+++ b/tests/test_images.py
@@ -346,3 +346,11 @@ def test_corrupted_jpeg_iss2266(pdf, pdf_name, images, images_name, filtr):
             print(fn)  # noqa: T201
             img = Image.open(BytesIO(zf.read(fn)))
             assert image_similarity(reader.pages[p].images[i].image, img) >= 0.99
+
+
+@pytest.mark.enable_socket()
+@pytest.mark.timeout(30)
+def test_large_compressed_image():
+    url = "https://github.com/py-pdf/pypdf/files/15306199/file_with_large_compressed_image.pdf"
+    reader = PdfReader(BytesIO(get_data_from_url(url, name="file_with_large_compressed_image.pdf")))
+    list(reader.pages[0].images)

Performance Bug: Reading large compressed images takes huge time to process
Reading a compressed image takes (>10minutes), if the image is large-ish (>250kb)

## Environment

Which environment were you using when you encountered the problem?

```bash
$ python -m platform
Linux-5.15.146.1-microsoft-standard-WSL2-x86_64-with-glibc2.35

$ python -c "import pypdf;print(pypdf._debug_versions)"
pypdf==4.2.0, crypt_provider=('pycryptodome', '3.19.0'), PIL=9.5.0
```

## Code + PDF

This is a minimal, complete example that shows the issue:

```python
import sys
from datetime import datetime

from pypdf import PdfReader

def log(msg):
    print(f"[{datetime.now()}] {msg}\n")

file = sys.argv[1]

log("Reading File PyPDF..")
images = []
pypdf_reader = PdfReader(file)
for pidx, page in enumerate(pypdf_reader.pages, start=1):
    log(f"Reading page {pidx}")
    for iidx, image in enumerate(page.images, start=1):
        log(f"Processing Image {iidx}")
        images.append(image.image)
    log(f"Competed page {pidx}")
log("Completed Reading File PyPDF!")
```

Attached is a sample PDF I created that can reproduce this error.
[file_with_image.pdf](https://github.com/py-pdf/pypdf/files/15305860/file_with_image.pdf)

The PDF can be added to the tests as well.

## Output

This is the complete output I see:
Look at the time difference between Image 6 and Image 7. It is close to 12 minutes

```
[2024-05-14 14:06:52.927018] Reading File PyPDF..
[2024-05-14 14:06:52.929346] Reading page 1
[2024-05-14 14:06:52.971993] Processing Image 1
[2024-05-14 14:06:52.993297] Processing Image 2
[2024-05-14 14:06:53.007304] Processing Image 3
[2024-05-14 14:06:53.021166] Processing Image 4
[2024-05-14 14:06:53.049529] Processing Image 5
[2024-05-14 14:06:53.051842] Processing Image 6

[2024-05-14 14:18:46.906472] Processing Image 7
[2024-05-14 14:18:47.088749] Processing Image 8
[2024-05-14 14:18:47.092159] Processing Image 9
[2024-05-14 14:18:47.099422] Processing Image 10
[2024-05-14 14:18:47.099513] Competed page 1
```


diff --git a/trl/trainer/utils.py b/trl/trainer/utils.py
index d1cc3a0e9d..1122086ca9 100644
--- a/trl/trainer/utils.py
+++ b/trl/trainer/utils.py
@@ -274,7 +274,7 @@ def __call__(self, examples: list[dict[str, Any]]) -> dict[str, torch.Tensor]:
             if "input_ids" not in example:
                 message = example[self.messages_key]
                 formatted_message = self.tokenizer.apply_chat_template(
-                    message, tokenize=False, add_generation_prompt=True
+                    message, tokenize=False, add_generation_prompt=False
                 )
                 tokenized_message = self.tokenizer(
                     formatted_message,

diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4d26819058..87404070a8 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -205,54 +205,74 @@ def setUp(self):
             ignore_index=self.ignore_index,
         )

-    # See https://github.com/huggingface/trl/pull/2287#discussion_r1856594421
-    @unittest.skip("This test must be updated.")
     def test_data_collator_for_chatml(self):
         # Process the data
         data = self.collator(self.examples)

+        # Verify basic shapes and types
+        self.assertIn("input_ids", data)
+        self.assertIn("attention_mask", data)
+        self.assertIn("labels", data)
+        self.assertIn("prompts", data)
+        self.assertIn("prompt_attention_mask", data)
+
         # Decode input_ids and labels for verification
         input_ids = data["input_ids"][0].tolist()
         labels = data["labels"][0].tolist()
         prompt_only = data["prompts"][0].tolist()

-        # Verify that input_ids start with optional padding tokens  and a single BOS token and there are no extra ones
-        first_non_pad = next(token for token in input_ids if token != self.tokenizer.pad_token_id)
-        self.assertEqual(
-            first_non_pad, self.bos_token_id, "The first non-padding token of input_ids should be BOS token."
-        )
-        self.assertEqual(input_ids.count(self.bos_token_id), 1, "There should be exactly one BOS token in input_ids.")
-
-        # Verify that the assistant's response token is present in input_ids and not in the prompt_only
-        last_assistant_response = self.examples[0][self.messages_key][-1]["content"]
-        last_assistant_response_tokens = self.tokenizer.encode(last_assistant_response, add_special_tokens=False)
-        response_in_input_ids = all(token in input_ids for token in last_assistant_response_tokens)
-        self.assertTrue(response_in_input_ids, "The assistant's response should be present in input_ids.")
+        # Get the last assistant's response for comparison
+        last_message = self.examples[0][self.messages_key][-1]
+        self.assertEqual(last_message["role"], "assistant", "Last message should be from assistant")
+        last_assistant_response = last_message["content"]

-        # Check if the last assistant's response tokens are not in prompt_only
-        response_in_prompt = all(token in prompt_only for token in last_assistant_response_tokens)
-        self.assertFalse(response_in_prompt, "The assistant's response should not be present in prompt_only.")
+        # Verify that input_ids contain both prompt and response
+        decoded_input = self.tokenizer.decode(input_ids)
+        self.assertIn(last_assistant_response, decoded_input, "Input should contain assistant's response")

-        # Verify that EOS token is at the end of input_ids
-        self.assertEqual(input_ids[-1], self.eos_token_id, "The last token of input_ids should be EOS token.")
+        # Verify that prompts only contain the conversation up to the last response
+        decoded_prompt = self.tokenizer.decode(prompt_only)
+        self.assertNotIn(last_assistant_response, decoded_prompt, "Prompt should not contain assistant's response")

-        # Verify that the labels preserved the target string (last_assistant_response)
-        last_assistant_response = self.examples[0][self.messages_key][-1]["content"]
-        last_assistant_response_tokens = self.tokenizer.encode(last_assistant_response, add_special_tokens=False)
+        # Verify labels are -100 for non-assistant parts
+        prompt_length = len(prompt_only)
+        self.assertTrue(
+            all(label == self.ignore_index for label in labels[:prompt_length]),
+            "Labels should be ignore_index for prompt tokens",
+        )

-        # Find the start and end of the last assistant's response in the labels
-        response_start = next(i for i, label in enumerate(labels) if label != self.ignore_index)
-        response_end = next(i for i in range(len(labels) - 1, -1, -1) if labels[i] != self.ignore_index)
+        # Verify labels match assistant response after prompt
+        # Add a filter to remove any trailing tokens after the first <|im_end|>
+        last_assistant_response_with_end = last_assistant_response + self.tokenizer.eos_token
+        last_assistant_response_tokens = self.tokenizer.encode(
+            last_assistant_response_with_end, add_special_tokens=False
+        )

-        actual_response = labels[response_start : response_end - 1]
+        response_labels = []
+        for label in labels[prompt_length:]:
+            if label == self.ignore_index:
+                continue
+            response_labels.append(label)
+            if label == self.tokenizer.convert_tokens_to_ids("<|im_end|>"):
+                break
         self.assertEqual(
-            actual_response,
+            response_labels,
             last_assistant_response_tokens,
-            "The labels should preserve the last assistant's response tokens.",
+            "Labels should match assistant response tokens",
         )

-        # Verify that EOS token is at the end of labels
-        self.assertEqual(labels[-1], self.eos_token_id, "The last token of labels should be EOS token.")
+        # Verify there isn't a generation prompt at the end
+        generation_prompt = "<|im_start|>assistant"
+        self.assertFalse(
+            decoded_input.strip().endswith(generation_prompt),
+            f"Input should not end with generation prompt '{generation_prompt}'",
+        )
+
+        self.assertEqual(
+            response_labels,
+            last_assistant_response_tokens,
+            "Labels should match assistant response tokens",
+        )

 class TestBatchGeneration(unittest.TestCase):

DataCollatorForChatML unexpected generation prompt
### System Info

- Platform: macOS-15.1.1-arm64-arm-64bit
- Python version: 3.10.15
- PyTorch version: 2.4.1
- CUDA device(s): not available
- Transformers version: 4.45.2
- Accelerate version: 1.0.1
- Accelerate config: not found
- Datasets version: 3.0.1
- HF Hub version: 0.25.2
- TRL version: 0.12.2
- bitsandbytes version: not installed
- DeepSpeed version: 0.15.2
- Diffusers version: 0.30.3
- Liger-Kernel version: not installed
- LLM-Blender version: not installed
- OpenAI version: 1.51.2
- PEFT version: 0.13.2

### Information

- [ ] The official example scripts
- [X] My own modified scripts

### Tasks

- [ ] An officially supported task in the `examples` folder
- [X] My own task or dataset (give details below)

### Reproduction

```python
from transformers import AutoTokenizer
from trl.trainer.utils import DataCollatorForChatML

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-1B-Instruct")
tokenizer.pad_token_id = tokenizer.eos_token_id
tokenizer.pad_token = tokenizer.eos_token

data_collator = DataCollatorForChatML(tokenizer)
examples = [
    {
        "messages": [
            {"role": "system", "content": "You are a professional translator."},
            {"role": "user", "content": "Hello!"},
            {"role": "assistant", "content": "Hi there! How can I help you today?"},
        ],
    },
]
batch = data_collator(examples)

print(tokenizer.decode(batch["input_ids"][0]))

label = batch["labels"][0]
label[label == -100] = tokenizer.eos_token_id
print(tokenizer.decode(label))
```

outputs:

```
<|begin_of_text|><|start_header_id|>system<|end_header_id|>

Cutting Knowledge Date: December 2023
Today Date: 07 Dec 2024

You are a professional translator.<|eot_id|><|start_header_id|>user<|end_header_id|>

Hello!<|eot_id|><|start_header_id|>assistant<|end_header_id|>

Hi there! How can I help you today?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!Hi there! How can I help you today?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
```

### Expected behavior

When processing instruction tuning, the model is not expected to generate `<|start_header_id|>assistant<|end_header_id|>` after `<|eot_id|>`. The correct model response should be `Hi there! How can I help you today?<|eot_id|>`.

We should change `trl/trainer/utils.py L276:277`

```python
formatted_message = self.tokenizer.apply_chat_template(
    # message, tokenize=False, add_generation_prompt=True,
    message, tokenize=False, add_generation_prompt=False
)
```

### Checklist

- [X] I have checked that my issue isn't already filed (see [open issues](https://github.com/huggingface/trl/issues?q=is%3Aissue))
- [X] I have included my system information
- [X] Any code provided is minimal, complete, and reproducible ([more on MREs](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks))
- [X] Any code provided is properly formatted in code blocks, (no screenshot, [more on code blocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks))
- [X] Any traceback provided is complete


diff --git a/trl/models/modeling_value_head.py b/trl/models/modeling_value_head.py
index 0797794013..592879ae3e 100644
--- a/trl/models/modeling_value_head.py
+++ b/trl/models/modeling_value_head.py
@@ -69,9 +69,6 @@ class AutoModelForCausalLMWithValueHead(PreTrainedModelWrapper):
     Class attributes:
         - **transformers_parent_class** (`transformers.PreTrainedModel`) -- The parent class of the wrapped model. This
             should be set to `transformers.AutoModelForCausalLM` for this class.
-        - **lm_head_namings** (`tuple`) -- A tuple of strings that are used to identify the language model head of the
-            wrapped model. This is set to `("lm_head", "embed_out", "output_layer")` for this class but can be changed
-            for other models in the future
         - **supported_args** (`tuple`) -- A tuple of strings that are used to identify the arguments that are supported
             by the `ValueHead` class. Currently, the supported args are:
             - **summary_dropout_prob** (`float`, `optional`, defaults to `None`) -- The dropout probability for the
@@ -86,7 +83,6 @@ class AutoModelForCausalLMWithValueHead(PreTrainedModelWrapper):
     """

     transformers_parent_class = AutoModelForCausalLM
-    lm_head_namings = ["lm_head", "embed_out", "output_layer"]
     supported_args = (
         "summary_dropout_prob",
         "v_head_initializer_range",
@@ -106,12 +102,7 @@ def __init__(self, pretrained_model, **kwargs):
         """
         super().__init__(pretrained_model, **kwargs)
         v_head_kwargs, _, _ = self._split_kwargs(kwargs)
-
-        if not any(hasattr(self.pretrained_model, attribute) for attribute in self.lm_head_namings):
-            raise ValueError("The model does not have a language model head, please use a model that has one.")
-
         self.v_head = ValueHead(self.pretrained_model.config, **v_head_kwargs)
-
         self._init_weights(**v_head_kwargs)

     def _init_weights(self, **kwargs):

diff --git a/tests/test_modeling_value_head.py b/tests/test_modeling_value_head.py
index ddc4eb850c..be4932e62f 100644
--- a/tests/test_modeling_value_head.py
+++ b/tests/test_modeling_value_head.py
@@ -265,14 +265,6 @@ def test_generate(self, model_name):
         # Just check if the generation works
         _ = model.generate(input_ids, generation_config=generation_config)

-    def test_raise_error_not_causallm(self):
-        # Test with a model without a LM head
-        model_id = "trl-internal-testing/tiny-GPT2LMHeadModel"
-        # This should raise a ValueError
-        with self.assertRaises(ValueError):
-            pretrained_model = AutoModelForCausalLM.from_pretrained(model_id)
-            _ = AutoModelForCausalLMWithValueHead.from_pretrained(pretrained_model.transformer)
-
     def test_transformers_bf16_kwargs(self):
         r"""
         Test if the transformers kwargs are correctly passed
@@ -283,10 +275,11 @@ def test_transformers_bf16_kwargs(self):
         for model_name in self.all_model_names:
             trl_model = self.trl_model_class.from_pretrained(model_name, torch_dtype=torch.bfloat16)

-            lm_head_namings = self.trl_model_class.lm_head_namings
+            lm_head_namings = ["lm_head", "embed_out", "output_layer"]

             self.assertTrue(
-                any(hasattr(trl_model.pretrained_model, lm_head_naming) for lm_head_naming in lm_head_namings)
+                any(hasattr(trl_model.pretrained_model, lm_head_naming) for lm_head_naming in lm_head_namings),
+                "Can't test the model because it doesn't have any of the expected lm_head namings",
             )

             for lm_head_naming in lm_head_namings:

Still not supporting for ChatGLM3 maybe
### System Info

trl    0.12.1
transformers      4.46.2

### Information

- [ ] The official example scripts
- [X] My own modified scripts

### Tasks

- [ ] An officially supported task in the `examples` folder
- [X] My own task or dataset (give details below)

### Reproduction

```python
from transformers import AutoTokenizer, AutoModel, BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments, AutoModelForCausalLM
import torch
import transformers
from peft import PeftModel
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead

model = AutoModelForCausalLMWithValueHead.from_pretrained("THUDM/chatglm3-6b-128k", trust_remote_code=True)
```

outputs:
```
Traceback (most recent call last):
  File "/home/fjy/folders/ERNIE2.0/finetune_chatglm6b.py", line 16, in <module>
    model = AutoModelForCausalLMWithValueHead.from_pretrained("THUDM/chatglm3-6b-128k", trust_remote_code=True)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/fjy/anaconda3/envs/env/lib/python3.12/site-packages/trl/models/modeling_base.py", line 233, in from_pretrained
    model = cls(pretrained_model, **trl_model_args)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/fjy/anaconda3/envs/env/lib/python3.12/site-packages/trl/models/modeling_value_head.py", line 107, in __init__
    raise ValueError("The model does not have a language model head, please use a model that has one.")
ValueError: The model does not have a language model head, please use a model that has one.
```

### Expected behavior

It's shown that ChatGLM3 doesnot have a language model head. Appearly it has.

### Checklist

- [X] I have checked that my issue isn't already filed (see [open issues](https://github.com/huggingface/trl/issues?q=is%3Aissue))
- [X] I have included my system information
- [X] Any code provided is minimal, complete, and reproducible ([more on MREs](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks))
- [X] Any code provided is properly formatted in code blocks, (no screenshot, [more on code blocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks))
- [X] Any traceback provided is complete


diff --git a/examples/scripts/ppo/ppo.py b/examples/scripts/ppo/ppo.py
index 19036ca7c1..e0dd07bb5a 100644
--- a/examples/scripts/ppo/ppo.py
+++ b/examples/scripts/ppo/ppo.py
@@ -14,6 +14,7 @@

 import shutil

+import torch
 from accelerate import PartialState
 from datasets import load_dataset
 from transformers import (
@@ -23,7 +24,15 @@
     HfArgumentParser,
 )

-from trl import ModelConfig, PPOConfig, PPOTrainer, ScriptArguments
+from trl import (
+    ModelConfig,
+    PPOConfig,
+    PPOTrainer,
+    ScriptArguments,
+    get_kbit_device_map,
+    get_peft_config,
+    get_quantization_config,
+)
 from trl.trainer.utils import SIMPLE_CHAT_TEMPLATE

@@ -67,6 +76,20 @@
     ################
     # Model & Tokenizer
     ################
+    torch_dtype = (
+        model_config.torch_dtype
+        if model_config.torch_dtype in ["auto", None]
+        else getattr(torch, model_config.torch_dtype)
+    )
+    quantization_config = get_quantization_config(model_config)
+    model_kwargs = dict(
+        revision=model_config.model_revision,
+        attn_implementation=model_config.attn_implementation,
+        torch_dtype=torch_dtype,
+        device_map=get_kbit_device_map() if quantization_config is not None else None,
+        quantization_config=quantization_config,
+    )
+
     tokenizer = AutoTokenizer.from_pretrained(
         model_config.model_name_or_path,
         padding_side="left",
@@ -81,12 +104,18 @@
     reward_model = AutoModelForSequenceClassification.from_pretrained(
         training_args.reward_model_path, trust_remote_code=model_config.trust_remote_code, num_labels=1
     )
-    ref_policy = AutoModelForCausalLM.from_pretrained(
-        training_args.sft_model_path, trust_remote_code=model_config.trust_remote_code
-    )
     policy = AutoModelForCausalLM.from_pretrained(
         training_args.sft_model_path, trust_remote_code=model_config.trust_remote_code
     )
+
+    peft_config = get_peft_config(model_config)
+    if peft_config is None:
+        ref_policy = AutoModelForCausalLM.from_pretrained(
+            training_args.sft_model_path, trust_remote_code=model_config.trust_remote_code
+        )
+    else:
+        ref_policy = None
+
     ################
     # Dataset
     ################
@@ -131,6 +160,7 @@ def tokenize(element):
         value_model=value_model,
         train_dataset=train_dataset,
         eval_dataset=eval_dataset,
+        peft_config=peft_config,
     )
     trainer.train()

diff --git a/examples/scripts/ppo/ppo_tldr.py b/examples/scripts/ppo/ppo_tldr.py
index fd20d6693d..73ea5cd852 100644
--- a/examples/scripts/ppo/ppo_tldr.py
+++ b/examples/scripts/ppo/ppo_tldr.py
@@ -14,6 +14,7 @@

 import shutil

+import torch
 from accelerate import PartialState
 from datasets import load_dataset
 from transformers import (
@@ -23,16 +24,24 @@
     HfArgumentParser,
 )

-from trl import ModelConfig, PPOConfig, PPOTrainer, ScriptArguments
+from trl import (
+    ModelConfig,
+    PPOConfig,
+    PPOTrainer,
+    ScriptArguments,
+    get_kbit_device_map,
+    get_peft_config,
+    get_quantization_config,
+)
 from trl.trainer.utils import SIMPLE_CHAT_TEMPLATE

 """
 python examples/scripts/ppo/ppo_tldr.py \
-    --dataset_name trl-internal-testing/tldr-preference-sft-trl-style
+    --dataset_name trl-internal-testing/tldr-preference-sft-trl-style \
     --dataset_test_split validation \
     --learning_rate 3e-6 \
-    --output_dir models/minimal/ppo \
+    --output_dir models/minimal/ppo_tldr \
     --per_device_train_batch_size 1 \
     --gradient_accumulation_steps 64 \
     --total_episodes 30000 \
@@ -41,11 +50,13 @@
     --reward_model_path cleanrl/EleutherAI_pythia-1b-deduped__reward__tldr \
     --missing_eos_penalty 1.0 \
     --stop_token eos \
-    --response_length 53
+    --response_length 53 \
+    --eval_strategy steps \
+    --eval_steps 100

 accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml \
     examples/scripts/ppo/ppo_tldr.py \
-    --dataset_name trl-internal-testing/tldr-preference-sft-trl-style
+    --dataset_name trl-internal-testing/tldr-preference-sft-trl-style \
     --dataset_test_split validation \
     --output_dir models/minimal/ppo_tldr \
     --learning_rate 3e-6 \
@@ -57,7 +68,9 @@
     --reward_model_path cleanrl/EleutherAI_pythia-1b-deduped__reward__tldr \
     --local_rollout_forward_batch_size 16 \
     --missing_eos_penalty 1.0 \
-    --stop_token eos
+    --stop_token eos \
+    --eval_strategy steps \
+    --eval_steps 100
 """

@@ -70,6 +83,20 @@
     ################
     # Model & Tokenizer
     ################
+    torch_dtype = (
+        model_config.torch_dtype
+        if model_config.torch_dtype in ["auto", None]
+        else getattr(torch, model_config.torch_dtype)
+    )
+    quantization_config = get_quantization_config(model_config)
+    model_kwargs = dict(
+        revision=model_config.model_revision,
+        attn_implementation=model_config.attn_implementation,
+        torch_dtype=torch_dtype,
+        device_map=get_kbit_device_map() if quantization_config is not None else None,
+        quantization_config=quantization_config,
+    )
+
     tokenizer = AutoTokenizer.from_pretrained(
         model_config.model_name_or_path,
         padding_side="left",
@@ -84,12 +111,18 @@
     reward_model = AutoModelForSequenceClassification.from_pretrained(
         training_args.reward_model_path, trust_remote_code=model_config.trust_remote_code, num_labels=1
     )
-    ref_policy = AutoModelForCausalLM.from_pretrained(
-        training_args.sft_model_path, trust_remote_code=model_config.trust_remote_code
-    )
     policy = AutoModelForCausalLM.from_pretrained(
         training_args.sft_model_path, trust_remote_code=model_config.trust_remote_code
     )
+
+    peft_config = get_peft_config(model_config)
+    if peft_config is None:
+        ref_policy = AutoModelForCausalLM.from_pretrained(
+            training_args.sft_model_path, trust_remote_code=model_config.trust_remote_code
+        )
+    else:
+        ref_policy = None
+
     ################
     # Dataset
     ################
@@ -138,6 +171,7 @@ def tokenize(element):
         value_model=value_model,
         train_dataset=train_dataset,
         eval_dataset=eval_dataset,
+        peft_config=peft_config,
     )
     trainer.train()

diff --git a/trl/trainer/ppo_config.py b/trl/trainer/ppo_config.py
index 25c2916025..465048184b 100644
--- a/trl/trainer/ppo_config.py
+++ b/trl/trainer/ppo_config.py
@@ -14,6 +14,7 @@

 import os
 from dataclasses import dataclass
+from typing import Optional

 from ..trainer.utils import OnPolicyConfig

@@ -32,6 +33,10 @@ class PPOConfig(OnPolicyConfig):
             Name of this experiment.
         reward_model_path (`str`, *optional*, defaults to `"EleutherAI/pythia-160m"`):
             Path to the reward model.
+        model_adapter_name (`Optional[str]`, *optional*, defaults to `None`):
+            Name of the train target PEFT adapter, when using LoRA with multiple adapters.
+        ref_adapter_name (`Optional[str]`, *optional*, defaults to `None`):
+            Name of the reference PEFT adapter, when using LoRA with multiple adapters.
         num_ppo_epochs (`int`, *optional*, defaults to `4`):
             Number of epochs to train.
         whiten_rewards (`bool`, *optional*, defaults to `False`):
@@ -52,6 +57,8 @@ class PPOConfig(OnPolicyConfig):

     exp_name: str = os.path.basename(__file__)[: -len(".py")]
     reward_model_path: str = "EleutherAI/pythia-160m"
+    model_adapter_name: Optional[str] = None
+    ref_adapter_name: Optional[str] = None
     num_ppo_epochs: int = 4
     whiten_rewards: bool = False
     kl_coef: float = 0.05
diff --git a/trl/trainer/ppo_trainer.py b/trl/trainer/ppo_trainer.py
index 8e2c514a54..2da4eaf981 100644
--- a/trl/trainer/ppo_trainer.py
+++ b/trl/trainer/ppo_trainer.py
@@ -18,6 +18,7 @@
 import textwrap
 import time
 from collections import defaultdict
+from contextlib import contextmanager, nullcontext
 from typing import Dict, List, Optional, Tuple, Union

 import numpy as np
@@ -44,9 +45,11 @@
 from transformers.integrations import get_reporting_integration_callbacks
 from transformers.trainer import DEFAULT_CALLBACKS, DEFAULT_PROGRESS_CALLBACK
 from transformers.trainer_callback import CallbackHandler, ExportableState, PrinterCallback
+from transformers.utils import is_peft_available
 from transformers.utils.deprecation import deprecate_kwarg

 from ..core import masked_mean, masked_whiten
+from ..models import create_reference_model
 from ..models.utils import unwrap_model_for_generation
 from ..trainer.utils import (
     OnlineTrainerState,
@@ -61,9 +64,12 @@
     truncate_response,
 )
 from .ppo_config import PPOConfig
-from .utils import generate_model_card
+from .utils import generate_model_card, peft_module_casting_to_bf16

+if is_peft_available():
+    from peft import PeftConfig, PeftModel, get_peft_model
+
 if is_wandb_available():
     import wandb

@@ -99,7 +105,7 @@ def __init__(
             Union[PreTrainedTokenizerBase, BaseImageProcessor, FeatureExtractionMixin, ProcessorMixin]
         ],
         policy: nn.Module,
-        ref_policy: nn.Module,
+        ref_policy: Optional[nn.Module],
         reward_model: nn.Module,
         train_dataset: Dataset,
         value_model: Optional[nn.Module] = None,
@@ -108,11 +114,12 @@ def __init__(
         # less commonly used
         optimizers: Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None),
         callbacks: Optional[List[TrainerCallback]] = None,
+        peft_config: Optional["PeftConfig"] = None,
     ) -> None:
         if ref_policy is policy:
             raise ValueError(
                 "`policy` and `ref_policy` cannot be the same object. If you want `ref_policy` to be the "
-                "same as `policy`, you must mass a copy of it, or `None` if you use peft."
+                "same as `policy`, you must make a copy of it, or `None` if you use peft."
             )

         self.args = config
@@ -125,7 +132,32 @@ def __init__(
         )
         self.policy.generation_config.pad_token_id = None  # generate tokens without truncation / padding

-        self.ref_policy = ref_policy
+        # peft support
+        if not is_peft_available() and peft_config is not None:
+            raise ImportError(
+                "PEFT is not installed and you passed a `peft_config` in the trainer's kwargs, please install it to use the PEFT models"
+            )
+        elif is_peft_available() and peft_config is not None:
+            # if model is a peft model and we have a peft_confg, we merge and unload it first
+            if isinstance(self.policy, PeftModel):
+                self.policy = self.policy.merge_and_unload()
+
+            # get peft model with the given config
+            self.policy = get_peft_model(self.policy, peft_config)
+            if args.bf16 and getattr(self.policy, "is_loaded_in_4bit", False):
+                peft_module_casting_to_bf16(self.policy)
+
+        self.is_peft_model = is_peft_available() and isinstance(self.policy, PeftModel)
+        self.model_adapter_name = args.model_adapter_name
+        self.ref_adapter_name = args.ref_adapter_name
+
+        if ref_policy:
+            self.ref_policy = ref_policy
+        elif self.is_peft_model:
+            self.ref_policy = None
+        else:
+            self.ref_policy = create_reference_model(self.policy)
+
         self.reward_model = reward_model
         self.train_dataset = train_dataset
         self.train_dataset_len = len(train_dataset)
@@ -174,12 +206,13 @@ def __init__(
         #########
         # setup model, optimizer, and others
         #########
-        for module in [policy, ref_policy, value_model, reward_model]:
-            disable_dropout_in_model(module)
+        for module in [self.policy, self.ref_policy, self.value_model, self.reward_model]:
+            if module is not None:
+                disable_dropout_in_model(module)
         if args.stop_token and args.stop_token == "eos":
             args.stop_token_id = processing_class.eos_token_id
-        self.model = PolicyAndValueWrapper(policy, value_model)
-        self.model.config = policy.config  # needed for pushing to hub
+        self.model = PolicyAndValueWrapper(self.policy, self.value_model)
+        self.model.config = self.policy.config  # needed for pushing to hub
         self.create_optimizer_and_scheduler(
             num_training_steps=args.num_total_batches
         )  # note that we are calling `self.lr_scheduler.step()` manually only at the batch level
@@ -244,11 +277,20 @@ def __init__(
             self.reward_model = prepare_deepspeed(
                 self.reward_model, args.per_device_train_batch_size, args.fp16, args.bf16
             )
-            self.ref_policy = prepare_deepspeed(
-                self.ref_policy, args.per_device_train_batch_size, args.fp16, args.bf16
-            )
+
+            if self.ref_policy is None:
+                if not self.is_peft_model:
+                    raise ValueError("No reference model and model is not a Peft model.")
+            else:
+                self.ref_policy = prepare_deepspeed(
+                    self.ref_policy, args.per_device_train_batch_size, args.fp16, args.bf16
+                )
         else:
-            self.ref_policy = self.ref_policy.to(self.accelerator.device)
+            if self.ref_policy is None:
+                if not self.is_peft_model:
+                    raise ValueError("No reference model and model is not a Peft model.")
+            else:
+                self.ref_policy = self.ref_policy.to(self.accelerator.device)
             self.reward_model = self.reward_model.to(self.accelerator.device)

     def get_train_dataloader(self) -> DataLoader:
@@ -257,6 +299,18 @@ def get_train_dataloader(self) -> DataLoader:
     def get_eval_dataloader(self) -> DataLoader:
         return self.eval_dataloader

+    @contextmanager
+    def null_ref_context(self):
+        """Context manager for handling null reference model (that is, peft adapter manipulation)."""
+        with self.accelerator.unwrap_model(
+            self.model.policy
+        ).disable_adapter() if self.is_peft_model and not self.ref_adapter_name else nullcontext():
+            if self.ref_adapter_name:
+                self.model.policy.set_adapter(self.ref_adapter_name)
+            yield
+            if self.ref_adapter_name:
+                self.model.policy.set_adapter(self.model_adapter_name or "default")
+
     def save_model(self, output_dir: Optional[str] = None, _internal_call: bool = False):
         backup_model = self.model
         self.model = self.model.policy  # save only the policy
@@ -368,7 +422,11 @@ def repeat_generator():
                     del logits, all_logprob
                     torch.cuda.empty_cache()

-                    ref_output = forward(ref_policy, query_response, processing_class.pad_token_id)
+                    if ref_policy is None:
+                        with self.null_ref_context():
+                            ref_output = forward(model.policy, query_response, processing_class.pad_token_id)
+                    else:
+                        ref_output = forward(ref_policy, query_response, processing_class.pad_token_id)
                     ref_logits = ref_output.logits[:, context_length - 1 : -1]
                     ref_logits /= args.temperature + 1e-7
                     ref_all_logprob = F.log_softmax(ref_logits, dim=-1)

diff --git a/tests/test_ppo_trainer.py b/tests/test_ppo_trainer.py
index c530f05b86..aacf0f2a1c 100644
--- a/tests/test_ppo_trainer.py
+++ b/tests/test_ppo_trainer.py
@@ -14,6 +14,8 @@
 import platform
 import subprocess

+from transformers.testing_utils import require_peft
+

 def test():
     command = """\
@@ -65,3 +67,34 @@ def test_num_train_epochs():
         shell=True,
         check=True,
     )
+
+
+@require_peft
+def test_peft_support():
+    command = """\
+python examples/scripts/ppo/ppo.py \
+    --dataset_name trl-internal-testing/descriptiveness-sentiment-trl-style \
+    --dataset_train_split descriptiveness \
+    --learning_rate 3e-6 \
+    --output_dir models/minimal/ppo \
+    --per_device_train_batch_size 4 \
+    --gradient_accumulation_steps 1 \
+    --total_episodes 10 \
+    --model_name_or_path EleutherAI/pythia-14m \
+    --missing_eos_penalty 1.0 \
+    --save_strategy no \
+    --stop_token eos \
+    --use_peft \
+    --lora_r 32 \
+    --lora_alpha 16 \
+    --lora_target_modules query_key_value dense
+"""
+    if platform.system() == "Windows":
+        # windows CI does not work with subprocesses for some reason
+        # e.g., https://github.com/huggingface/trl/actions/runs/9600036224/job/26475286210?pr=1743
+        return
+    subprocess.run(
+        command,
+        shell=True,
+        check=True,
+    )

How to Add PEFT to PPO Trainer or PPO Config
I am trying to realize RLHF through PPO.

May I ask how can I realize PEFT in RLHF/PPO. I can see this parameter in DPOTrainer. However, I cannot see that in PPOTrainer.

PPO does not yet support PEFT. But it may be a good enhancement.
Hi @qgallouedec, I'd love to contribute to this enhancement! I noticed that this issue has been open for three months, and I'd like to help bring it forward.

Is there anything specific I should be mindful of as I start on this? Any guidance or notes on the current status would be greatly appreciated.

Thank you!


diff --git a/rq/job.py b/rq/job.py
index 918f7fdc1..7a964aef0 100644
--- a/rq/job.py
+++ b/rq/job.py
@@ -729,6 +729,10 @@ def set_id(self, value: str) -> None:
         """
         if not isinstance(value, str):
             raise TypeError('id must be a string, not {0}'.format(type(value)))
+
+        if ":" in value:
+            raise ValueError('id must not contain ":"')
+
         self._id = value

     def heartbeat(self, timestamp: datetime, ttl: int, pipeline: Optional['Pipeline'] = None, xx: bool = False):

diff --git a/tests/test_job.py b/tests/test_job.py
index 36c356e50..9f93cfbf8 100644
--- a/tests/test_job.py
+++ b/tests/test_job.py
@@ -900,6 +900,13 @@ def test_create_job_with_id(self):

         self.assertRaises(TypeError, queue.enqueue, fixtures.say_hello, job_id=1234)

+    def test_create_job_with_invalid_id(self):
+        """test creating jobs with a custom invalid ID (with character :)"""
+        queue = Queue(connection=self.connection)
+
+        with self.assertRaises(ValueError):
+            queue.enqueue(fixtures.say_hello, job_id="1234:4321")
+
     def test_create_job_with_async(self):
         """test creating jobs with async function"""
         queue = Queue(connection=self.connection)

Dangling job occurs when job_id contains the character ":"
Hi,

After updating to v2.0.0, job_id cannot contain the character ":"; otherwise, the job is never picked up or started by a worker.

I'll investigate this over the next few days, but I suspect that [PR #1964](https://github.com/rq/rq/pull/1964) might restrict the job_id format. We may need to add validation when creating jobs to address this.

Thanks, mind opening a PR when you're free?


diff --git a/Cython/Compiler/FusedNode.py b/Cython/Compiler/FusedNode.py
index 1b0ceb72ca0..9cb5f4e9059 100644
--- a/Cython/Compiler/FusedNode.py
+++ b/Cython/Compiler/FusedNode.py
@@ -643,11 +643,25 @@ def _fused_signature_index(self, pyx_code):
         Generate Cython code for constructing a persistent nested dictionary index of
         fused type specialization signatures.
         """
+        # Note on thread-safety:
+        # Filling in "fused_sigindex" should only happen once. However, in a multi-threaded
+        # environment it's possible that multiple threads can all start to fill it in
+        # independently (especially on freehtreading builds).
+        # Therefore:
+        # * "_fused_sigindex_ref" is a list of length 1 where the first element is either None,
+        #   or a dictionary of signatures to lookup.
+        # * We rely on being able to get/set list elements atomically (which is true on
+        #   freethreading and regular Python).
+        # * It doesn't really matter if multiple threads start generating their own version
+        #   of this - the contents will end up the same. The main point is that no thread
+        #   sees a half filled-in sigindex
         pyx_code.put_chunk(
             """
-                if not _fused_sigindex:
+                fused_sigindex = <dict> _fused_sigindex_ref[0]
+                if fused_sigindex is None:
+                    fused_sigindex = {}
                     for sig in <dict> signatures:
-                        sigindex_node = <dict> _fused_sigindex
+                        sigindex_node = fused_sigindex
                         *sig_series, last_type = sig.strip('()').split('|')
                         for sig_type in sig_series:
                             if sig_type not in sigindex_node:
@@ -655,6 +669,7 @@ def _fused_signature_index(self, pyx_code):
                             else:
                                 sigindex_node = <dict> sigindex_node[sig_type]
                         sigindex_node[last_type] = sig
+                    _fused_sigindex_ref[0] = fused_sigindex
             """
         )

@@ -694,7 +709,7 @@ def make_fused_cpdef(self, orig_py_func, env, is_def):

         pyx_code.put_chunk(
             """
-                def __pyx_fused_cpdef(signatures, args, kwargs, defaults, _fused_sigindex={}):
+                def __pyx_fused_cpdef(signatures, args, kwargs, defaults, _fused_sigindex_ref=[None]):
                     # FIXME: use a typed signature - currently fails badly because
                     #        default arguments inherit the types we specify here!

@@ -773,7 +788,7 @@ def __pyx_fused_cpdef(signatures, args, kwargs, defaults, _fused_sigindex={}):
         pyx_code.put_chunk(
             """
                 sigindex_matches = []
-                sigindex_candidates = [_fused_sigindex]
+                sigindex_candidates = [fused_sigindex]

                 for dst_type in dest_sig:
                     found_matches = []

diff --git a/tests/run/fused_def.pyx b/tests/run/fused_def.pyx
index 96fcb93ea2d..2da9ddb89cb 100644
--- a/tests/run/fused_def.pyx
+++ b/tests/run/fused_def.pyx
@@ -461,3 +461,53 @@ cdef class HasBound:
     func = bind_me[float]

     func_fused = bind_me
+
+
+
+ctypedef fused IntOrFloat1:
+    int
+    float
+
+ctypedef fused IntOrFloat2:
+    int
+    float
+
+ctypedef fused IntOrFloat3:
+    int
+    float
+
+def really_simple_fused_function(IntOrFloat1 a, IntOrFloat2 b, IntOrFloat3 c):
+    # Don't use this function for anything except the thread safety stress test.
+    # The first call should be from that.
+    return (a + 1) * 2 + (b*c)
+
+def run_really_simple_fused_function(start_barrier, n_iters, failed_list):
+    # Maximize the chance of failure by waiting until all threads are ready to start
+    args = [ n if n % 2 else float(n) for n in range(n_iters) ]
+    try:
+        start_barrier.wait()
+        for a in args:
+            really_simple_fused_function(a, a, a)
+    except:
+        failed_list.append(True)
+
+
+def stress_test_thread_safety(n_threads):
+    """
+    >>> stress_test_thread_safety(20)
+    """
+    from threading import Barrier, Thread
+    start_barrier = Barrier(n_threads)
+
+    failed_list = []
+
+    threads = [
+        Thread(
+            target=run_really_simple_fused_function,
+            args=[start_barrier, 30, failed_list]
+        ) for _ in range(n_threads) ]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+    assert not failed_list, len(failed_list)

[BUG] No matching signature found on free-threaded Python code generation
### Describe the bug

Whilst testing SciPy for issues against free-threaded CPython 3.13 https://github.com/scipy/scipy/pull/21496, we have observed several instances of errors on functions that fuse dtypes, for some reason, these errors are being flagged when running tests under parallel conditions, and not under single threading.

```
__________________________________________________________________ test_label03[numpy] __________________________________________________________________
scipy/ndimage/tests/test_measurements.py:169: in test_label03
    out, n = ndimage.label(data)
        data       = array([1.])
        xp         = <module 'numpy' from '/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/numpy/__init__.py'>
scipy/ndimage/_support_alternative_backends.py:37: in wrapper
    result = func(*args, **kwds)
        args       = (array([1.]),)
        delegator  = <function binary_fill_holes_signature at 0x385421dfa00>
        func       = <function label at 0x385421de9c0>
        kwds       = {}
        module_name = 'ndimage'
        xp         = <module 'scipy._lib.array_api_compat.numpy' from '/home/rgommers/code/pixi-dev-scipystack/scipy/scipy/build-nogil-install/lib/python3.13t/site-packages/scipy/_lib/array_api_compat/numpy/__init__.py'>
scipy/ndimage/_measurements.py:218: in label
    max_label = _ni_label._label(input, structure, output)
        caller_provided_output = False
        ii         = 3
        input      = array([1.])
        need_64bits = False
        output     = array([-889028608], dtype=int32)
        structure  = array([ True,  True,  True])
_ni_label.pyx:200: in _ni_label._label
    ???
_ni_label.pyx:237: in _ni_label._label
    ???
_ni_label.pyx:93: in _ni_label.__pyx_fused_cpdef
    ???
E   TypeError: No matching signature found
```

The generated C code for the Cython instance is described in the following comment: https://github.com/scipy/scipy/pull/21496#discussion_r1850947457

I will provide a minimal reproducible example soon.

cc @rgommers

### Code to reproduce the behaviour:

_No response_

### Expected behaviour

_No response_

### OS

Linux

### Python version

3.13t

### Cython version

_No response_

### Additional context

_No response_

There's definitely some caching inside the fused dispatch function (so it's quicker to match on subsequent calls). On a quick look, I'm not convinced that caching is currently thread-safe. I'll look into it.

(There's also some code that imports numpy and gets the dtype. That's less likely to be an issue, but probably still needs a close look)
> The generated C code for the Cython instance is described in the following comment: [...]

That's probably actually a lower level than it's worth looking at. In this case it goes "fused function"->"generated Cython code"->"generated C code" and the issue is in the intermediate Cython code.

I think this should apply to any cpdef or def fused function so there isn't too much value trying to prepare a specific reproducible example.

defect
Code Generation
freethreading CPython


diff --git a/sqlglot/optimizer/qualify_columns.py b/sqlglot/optimizer/qualify_columns.py
index ceb9ceb44f..871bb365ba 100644
--- a/sqlglot/optimizer/qualify_columns.py
+++ b/sqlglot/optimizer/qualify_columns.py
@@ -898,10 +898,22 @@ def get_source_columns(self, name: str, only_visible: bool = False) -> t.Sequenc
                     for (name, alias) in itertools.zip_longest(columns, column_aliases)
                 ]

+            pseudocolumns = self._get_source_pseudocolumns(name)
+            if pseudocolumns:
+                columns = list(columns)
+                columns.extend(c for c in pseudocolumns if c not in columns)
+
             self._get_source_columns_cache[cache_key] = columns

         return self._get_source_columns_cache[cache_key]

+    def _get_source_pseudocolumns(self, name: str) -> t.Sequence[str]:
+        if self.schema.dialect == "snowflake" and self.scope.expression.args.get("connect"):
+            # When there is a CONNECT BY clause, there is only one table being scanned
+            # See: https://docs.snowflake.com/en/sql-reference/constructs/connect-by
+            return ["LEVEL"]
+        return []
+
     def _get_all_source_columns(self) -> t.Dict[str, t.Sequence[str]]:
         if self._source_columns is None:
             self._source_columns = {

diff --git a/tests/fixtures/optimizer/qualify_columns.sql b/tests/fixtures/optimizer/qualify_columns.sql
index eeaf8b3555..ecb6eee5ce 100644
--- a/tests/fixtures/optimizer/qualify_columns.sql
+++ b/tests/fixtures/optimizer/qualify_columns.sql
@@ -205,6 +205,30 @@ SELECT x.a + 1 AS i, missing_column AS missing_column FROM x AS x;
 SELECT s, arr1, arr2 FROM arrays_test LEFT ARRAY JOIN arr1, arrays_test.arr2;
 SELECT arrays_test.s AS s, arrays_test.arr1 AS arr1, arrays_test.arr2 AS arr2 FROM arrays_test AS arrays_test LEFT ARRAY JOIN arrays_test.arr1, arrays_test.arr2;

+# execute: false
+# dialect: snowflake
+WITH employees AS (
+    SELECT *
+    FROM (VALUES ('President', 1, NULL),
+                 ('Vice President Engineering', 10, 1),
+                 ('Programmer', 100, 10),
+                 ('QA Engineer', 101, 10),
+                 ('Vice President HR', 20, 1),
+                 ('Health Insurance Analyst', 200, 20)
+    ) AS t(title, employee_ID, manager_ID)
+)
+SELECT
+  employee_ID,
+  manager_ID,
+  title,
+  level
+FROM employees
+START WITH title = 'President'
+CONNECT BY manager_ID = PRIOR employee_id
+ORDER BY
+  employee_ID NULLS LAST;
+WITH EMPLOYEES AS (SELECT T.TITLE AS TITLE, T.EMPLOYEE_ID AS EMPLOYEE_ID, T.MANAGER_ID AS MANAGER_ID FROM (VALUES ('President', 1, NULL), ('Vice President Engineering', 10, 1), ('Programmer', 100, 10), ('QA Engineer', 101, 10), ('Vice President HR', 20, 1), ('Health Insurance Analyst', 200, 20)) AS T(TITLE, EMPLOYEE_ID, MANAGER_ID)) SELECT EMPLOYEES.EMPLOYEE_ID AS EMPLOYEE_ID, EMPLOYEES.MANAGER_ID AS MANAGER_ID, EMPLOYEES.TITLE AS TITLE, EMPLOYEES.LEVEL AS LEVEL FROM EMPLOYEES AS EMPLOYEES START WITH EMPLOYEES.TITLE = 'President' CONNECT BY EMPLOYEES.MANAGER_ID = PRIOR EMPLOYEES.EMPLOYEE_ID ORDER BY EMPLOYEE_ID;
+
 --------------------------------------
 -- Derived tables
 --------------------------------------

`sqlglot.optimizer.qualify.qualify` fails when using `CONNECT BY` + `level` from a CTE
## Fully reproducible code snippet
In Snowflake, the `SELECT FROM <data_source> START WITH ... CONNECT BY` construct allows a `<level_expression>` by basically allowing to select a `level` column in addition to the existing columns:

```sql
SELECT
  employee_ID,
  manager_ID,
  title,
  level --<-- This isn't a column from "employees", but a column allowed by Snowflake in `CONNECT BY` constructs
FROM employees
START WITH title = 'President'
CONNECT BY manager_ID = PRIOR employee_id
ORDER BY
  employee_ID NULLS LAST
```

`sqlglot.optimizer.qualify.qualify` handles this fine ✅

However, if the `SELECT` is `FROM` a CTE, it fails!

Taking the same example as before **but** putting all the data in a CTE (instead of a table) breaks `qualify`:
```py
from sqlglot import parse_one
from sqlglot.optimizer.qualify import qualify

SQL_DIALECT = "snowflake"

print(qualify(parse_one("""
WITH employees AS (
    SELECT *
    FROM (VALUES ('President', 1, NULL),
                 ('Vice President Engineering', 10, 1),
                 ('Programmer', 100, 10),
                 ('QA Engineer', 101, 10),
                 ('Vice President HR', 20, 1),
                 ('Health Insurance Analyst', 200, 20)
    ) AS t(title, employee_ID, manager_ID)
)
SELECT
  employee_ID,
  manager_ID,
  title,
  level
FROM employees
START WITH title = 'President'
CONNECT BY manager_ID = PRIOR employee_id
ORDER BY
  employee_ID NULLS LAST
""", dialect="snowflake"), schema=None, dialect="snowflake").sql(pretty=True, dialect=SQL_DIALECT))
```

Output:
```
Traceback (most recent call last):
  File "test_sqlglot.py", line 7, in <module>
    print(qualify(parse_one("""
          ^^^^^^^^^^^^^^^^^^^^^
  File "/sqlglot/optimizer/qualify.py", line 102, in qualify
    validate_qualify_columns_func(expression)
  File "sqlglot/optimizer/qualify_columns.py", line 109, in validate_qualify_columns
    raise OptimizeError(f"Column '{column}' could not be resolved{for_table}")
sqlglot.errors.OptimizeError: Column '"LEVEL"' could not be resolved
```

## Official Documentation
(see [documentation](https://docs.snowflake.com/en/sql-reference/constructs/connect-by#examples)).


diff --git a/sqlglot/dialects/tsql.py b/sqlglot/dialects/tsql.py
index 856142edc4..1acc5ca8a7 100644
--- a/sqlglot/dialects/tsql.py
+++ b/sqlglot/dialects/tsql.py
@@ -232,7 +232,7 @@ def _builder(args: t.List) -> E:
         if start_date and start_date.is_number:
             # Numeric types are valid DATETIME values
             if start_date.is_int:
-                adds = DEFAULT_START_DATE + datetime.timedelta(days=int(start_date.this))
+                adds = DEFAULT_START_DATE + datetime.timedelta(days=start_date.to_py())
                 start_date = exp.Literal.string(adds.strftime("%F"))
             else:
                 # We currently don't handle float values, i.e. they're not converted to equivalent DATETIMEs.

diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index 61365994bd..e8cd69648b 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -1579,6 +1579,11 @@ def test_date_diff(self):
             },
         )

+        self.validate_identity(
+            "SELECT DATEADD(DAY, DATEDIFF(DAY, -3, GETDATE()), '08:00:00')",
+            "SELECT DATEADD(DAY, DATEDIFF(DAY, CAST('1899-12-29' AS DATETIME2), CAST(GETDATE() AS DATETIME2)), '08:00:00')",
+        )
+
     def test_lateral_subquery(self):
         self.validate_all(
             "SELECT x.a, x.b, t.v, t.y FROM x CROSS APPLY (SELECT v, y FROM t) t(v, y)",

TSQL fails on parsing DATEDIFF function with literals
Hello,
I just discovered that this code generates an error while parsing DATEDIFF second argument:
```
import sqlglot
statement = """
SELECT DATEADD(DAY, DATEDIFF(DAY, -3, CREATION_TIME_NEW), '08:00:00')
FROM y
"""

ast = sqlglot.parse_one(statement, 'tsql')
print(ast.sql('tsql'))
```

Error:
```
Python312\Lib\site-packages\sqlglot\dialects\tsql.py:235, in _build_date_delta.<locals>._builder(args)

    232 if start_date and start_date.is_number:
    233     # Numeric types are valid DATETIME values
    234     if start_date.is_int:
--> 235         adds = DEFAULT_START_DATE + datetime.timedelta(days=int(start_date.this))
    236         start_date = exp.Literal.string(adds.strftime("%F"))
    237     else:
    238         # We currently don't handle float values, i.e. they're not converted to equivalent DATETIMEs.
    239         # This is not a problem when generating T-SQL code, it is when transpiling to other dialects.

TypeError: int() argument must be a string, a bytes-like object or a real number, not 'Literal'
```
start_date at this point is:
```
Neg(
  this=Literal(this=3, is_string=False))
```

So `int(start_date.this)` fails.


diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index fbbc8f14d4..48b59ecfaa 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -4620,14 +4620,14 @@ def _parse_interval(self, match_interval: bool = True) -> t.Optional[exp.Add | e
             this = exp.Literal.string(this.to_py())
         elif this and this.is_string:
             parts = exp.INTERVAL_STRING_RE.findall(this.name)
-            if len(parts) == 1:
-                if unit:
-                    # Unconsume the eagerly-parsed unit, since the real unit was part of the string
-                    self._retreat(self._index - 1)
+            if parts and unit:
+                # Unconsume the eagerly-parsed unit, since the real unit was part of the string
+                unit = None
+                self._retreat(self._index - 1)

+            if len(parts) == 1:
                 this = exp.Literal.string(parts[0][0])
                 unit = self.expression(exp.Var, this=parts[0][1].upper())
-
         if self.INTERVAL_SPANS and self._match_text_seq("TO"):
             unit = self.expression(
                 exp.IntervalSpan, this=unit, expression=self._parse_var(any_token=True, upper=True)

diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
index 204151ed38..acdb2d4a83 100644
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -71,6 +71,9 @@ def test_postgres(self):
         self.validate_identity("EXEC AS myfunc @id = 123", check_command_warning=True)
         self.validate_identity("SELECT CURRENT_USER")
         self.validate_identity("SELECT * FROM ONLY t1")
+        self.validate_identity(
+            "SELECT * FROM t WHERE some_column >= CURRENT_DATE + INTERVAL '1 day 1 hour' AND some_another_column IS TRUE"
+        )
         self.validate_identity(
             """UPDATE "x" SET "y" = CAST('0 days 60.000000 seconds' AS INTERVAL) WHERE "x"."id" IN (2, 3)"""
         )

interval parsing for postgresql
Hi! I have some problems with parsing postgresql statements that has more that one date units in intervals...
lib version 25.34.0
```python3
import sqlglot

sql = """
select *
from table
where
    some_column >= (current_date + interval '1 day 1 hour')
    and some_another_column is True;
"""

parsed = sqlglot.parse_one(sql, dialect="postgres")
```
This works fine and we have
```
expression=Paren(
    this=Add(
        this=CurrentDate(),
        expression=Interval(this=Literal(this=1 day 1 hour, is_string=True))
    )
)
```
But if we drop brackets in where statement
```sql
select *
from table
where
    some_column >= current_date + interval '1 day 1 hour'
    and some_another_column is True;
```
parser throw exception `sqlglot.errors.ParseError: Invalid expression / Unexpected token. Line 6, Col: 24.`
```
expression=Add(
    this=CurrentDate(),
    expression=Interval(
        this=Literal(this=1 day 1 hour, is_string=True),
        unit=Var(this=AND)
    )
)
```

**Official Documentation**
https://www.postgresql.org/docs/current/functions-datetime.html

Shall I check this?
One more example that has the expected result, but strange (for postgres) syntax
```sql
select *
from table
where
    some_column >= current_date + (interval '1 day 1 hour')
    and some_another_column is True;
```
@ankur334 Feel free to take a stab at it if that's what you mean, thank you!
Sure @georgesittas and @VaggelisD I am looking into this. I hope to solve it. I will inform the team if I get stuck somewhere
Really appreciate it @ankur334, don't hesitate to ping us on slack for quicker iterations.


diff --git a/sqlglot/optimizer/qualify_columns.py b/sqlglot/optimizer/qualify_columns.py
index 4880a615c6..ca5684407c 100644
--- a/sqlglot/optimizer/qualify_columns.py
+++ b/sqlglot/optimizer/qualify_columns.py
@@ -75,7 +75,7 @@ def qualify_columns(
         if not schema.empty and expand_alias_refs:
             _expand_alias_refs(scope, resolver)

-        if not isinstance(scope.expression, exp.UDTF):
+        if isinstance(scope.expression, exp.Select):
             if expand_stars:
                 _expand_stars(
                     scope,

diff --git a/tests/fixtures/optimizer/qualify_columns.sql b/tests/fixtures/optimizer/qualify_columns.sql
index 2640145bed..735c71a5e7 100644
--- a/tests/fixtures/optimizer/qualify_columns.sql
+++ b/tests/fixtures/optimizer/qualify_columns.sql
@@ -190,6 +190,10 @@ SELECT x._col_0 AS _col_0, x._col_1 AS _col_1 FROM (VALUES (1, 2)) AS x(_col_0,
 SELECT SOME_UDF(data).* FROM t;
 SELECT SOME_UDF(t.data).* FROM t AS t;

+# execute: false
+SELECT p.* FROM p UNION ALL SELECT p2.* FROM p2;
+SELECT p.* FROM p AS p UNION ALL SELECT p2.* FROM p2 AS p2;
+
 # execute: false
 # allow_partial_qualification: true
 # validate_qualify_columns: false

`sqlglot.optimizer.qualify_columns.qualify_columns` fails on `table.*` inside `UNION ALL`
### Explanation

When using `qualify_columns` on a SQL query that contains a `UNION ALL` and one of the statement uses the `SELECT table.*` notation, `qualify_columns` raises an `sqlglot.errors.OptimizeError: Unknown table: p` error.

### Fully reproducible code snippet

```py
from sqlglot import parse_one
from sqlglot.schema import MappingSchema
from sqlglot.optimizer.qualify_columns import qualify_columns

SQL_DIALECT = "snowflake"

print(qualify_columns(parse_one("""
SELECT p.*
FROM p

UNION ALL

SELECT p2.*
FROM p2
""", dialect="snowflake"), schema=MappingSchema(dialect=SQL_DIALECT)).sql(pretty=True, dialect=SQL_DIALECT))
```

Running this snippet raises an error:
```
Traceback (most recent call last):
  File "test_sqlglot.py", line 7, in <module>
    print(qualify_columns(parse_one("""
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "sqlglot/optimizer/qualify_columns.py", line 81, in qualify_columns
    _expand_stars(
  File "sqlglot/optimizer/qualify_columns.py", line 623, in _expand_stars
    raise OptimizeError(f"Unknown table: {table}")
sqlglot.errors.OptimizeError: Unknown table: p
```

Looks like, inside `_expand_stars`, `scope.sources` is an empty list. In that exact case, `scope` represents the entire query (with both SELECT and the UNION ALL in between), and `scope_type` is ROOT (not UNION).

Again, happy to make a PR.


diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 39975d7d57..150cbe1af4 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -6665,6 +6665,11 @@ class Week(Func):
     arg_types = {"this": True, "mode": False}

+class XMLElement(Func):
+    _sql_names = ["XMLELEMENT"]
+    arg_types = {"this": True, "expressions": False}
+
+
 class XMLTable(Func):
     arg_types = {"this": True, "passing": False, "columns": False, "by_ref": False}

diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 635a5f5fa7..5ba3f57ce0 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -4612,3 +4612,7 @@ def includeproperty_sql(self, expression: exp.IncludeProperty) -> str:
             include = f"{include} AS {alias}"

         return include
+
+    def xmlelement_sql(self, expression: exp.XMLElement) -> str:
+        name = f"NAME {self.sql(expression, 'this')}"
+        return self.func("XMLELEMENT", name, *expression.expressions)
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 4cd3d30560..fbbc8f14d4 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1143,6 +1143,11 @@ class Parser(metaclass=_Parser):
         "TRIM": lambda self: self._parse_trim(),
         "TRY_CAST": lambda self: self._parse_cast(False, safe=True),
         "TRY_CONVERT": lambda self: self._parse_convert(False, safe=True),
+        "XMLELEMENT": lambda self: self.expression(
+            exp.XMLElement,
+            this=self._match_text_seq("NAME") and self._parse_id_var(),
+            expressions=self._match(TokenType.COMMA) and self._parse_csv(self._parse_expression),
+        ),
     }

     QUERY_MODIFIER_PARSERS = {
@@ -5351,18 +5356,21 @@ def _parse_function_call(
                 functions = self.FUNCTIONS

             function = functions.get(upper)
+            known_function = function and not anonymous

-            alias = upper in self.FUNCTIONS_WITH_ALIASED_ARGS
+            alias = not known_function or upper in self.FUNCTIONS_WITH_ALIASED_ARGS
             args = self._parse_csv(lambda: self._parse_lambda(alias=alias))

-            if alias:
+            if alias and known_function:
                 args = self._kv_to_prop_eq(args)

-            if function and not anonymous:
-                if "dialect" in function.__code__.co_varnames:
-                    func = function(args, dialect=self.dialect)
+            if known_function:
+                func_builder = t.cast(t.Callable, function)
+
+                if "dialect" in func_builder.__code__.co_varnames:
+                    func = func_builder(args, dialect=self.dialect)
                 else:
-                    func = function(args)
+                    func = func_builder(args)

                 func = self.validate_expression(func, args)
                 if self.dialect.PRESERVE_ORIGINAL_NAMES:
@@ -6730,7 +6738,9 @@ def _parse_expressions(self) -> t.List[exp.Expression]:

     def _parse_select_or_expression(self, alias: bool = False) -> t.Optional[exp.Expression]:
         return self._parse_select() or self._parse_set_operations(
-            self._parse_expression() if alias else self._parse_assignment()
+            self._parse_alias(self._parse_assignment(), explicit=True)
+            if alias
+            else self._parse_assignment()
         )

     def _parse_ddl_select(self) -> t.Optional[exp.Expression]:

diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
index 66ded239a3..204151ed38 100644
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -1289,3 +1289,17 @@ def test_array_length(self):
                 "clickhouse": UnsupportedError,
             },
         )
+
+    def test_xmlelement(self):
+        self.validate_identity("SELECT XMLELEMENT(NAME foo)")
+        self.validate_identity("SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('xyz' AS bar))")
+        self.validate_identity("SELECT XMLELEMENT(NAME test, XMLATTRIBUTES(a, b)) FROM test")
+        self.validate_identity(
+            "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES(CURRENT_DATE AS bar), 'cont', 'ent')"
+        )
+        self.validate_identity(
+            """SELECT XMLELEMENT(NAME "foo$bar", XMLATTRIBUTES('xyz' AS "a&b"))"""
+        )
+        self.validate_identity(
+            "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('xyz' AS bar), XMLELEMENT(NAME abc), XMLCOMMENT('test'), XMLELEMENT(NAME xyz))"
+        )

[Postgres] Parse error for XMLElement
Postgres XMLElement returns parse error:
```
SELECT xmlelement(name foo);
# > <foo/>
```

**Fully reproducible code snippet**
```
from sqlglot import parse_one
from sqlglot.optimizer.qualify import qualify

qualify(parse_one(sql = """
 SELECT xmlelement(name "foo")
""", dialect="postgres"), schema=None)

# > sqlglot.errors.ParseError: Expecting ). Line 2, Col: 29.
```

**Official Documentation**
https://www.postgresql.org/docs/current/functions-xml.html#FUNCTIONS-PRODUCING-XML-XMLELEMENT


diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 842df5a753..9aee03c1ca 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -2399,18 +2399,21 @@ def ordered_sql(self, expression: exp.Ordered) -> str:
                     f"'{nulls_sort_change.strip()}' translation not supported in window functions"
                 )
                 nulls_sort_change = ""
-            elif (
-                self.NULL_ORDERING_SUPPORTED is False
-                and (isinstance(expression.find_ancestor(exp.AggFunc, exp.Select), exp.AggFunc))
-                and (
-                    (asc and nulls_sort_change == " NULLS LAST")
-                    or (desc and nulls_sort_change == " NULLS FIRST")
-                )
+            elif self.NULL_ORDERING_SUPPORTED is False and (
+                (asc and nulls_sort_change == " NULLS LAST")
+                or (desc and nulls_sort_change == " NULLS FIRST")
             ):
-                self.unsupported(
-                    f"'{nulls_sort_change.strip()}' translation not supported for aggregate functions with {sort_order} sort order"
-                )
-                nulls_sort_change = ""
+                # BigQuery does not allow these ordering/nulls combinations when used under
+                # an aggregation func or under a window containing one
+                ancestor = expression.find_ancestor(exp.AggFunc, exp.Window, exp.Select)
+
+                if isinstance(ancestor, exp.Window):
+                    ancestor = ancestor.this
+                if isinstance(ancestor, exp.AggFunc):
+                    self.unsupported(
+                        f"'{nulls_sort_change.strip()}' translation not supported for aggregate functions with {sort_order} sort order"
+                    )
+                    nulls_sort_change = ""
             elif self.NULL_ORDERING_SUPPORTED is None:
                 if expression.this.is_int:
                     self.unsupported(

diff --git a/tests/dialects/test_bigquery.py b/tests/dialects/test_bigquery.py
index 80a5dcc387..ec16dba243 100644
--- a/tests/dialects/test_bigquery.py
+++ b/tests/dialects/test_bigquery.py
@@ -2131,6 +2131,16 @@ def test_null_ordering(self):
                 },
             )

+            self.validate_all(
+                f"SELECT SUM(f1) OVER (ORDER BY f2 {sort_order}) FROM t",
+                read={
+                    "": f"SELECT SUM(f1) OVER (ORDER BY f2 {sort_order} {null_order}) FROM t",
+                },
+                write={
+                    "bigquery": f"SELECT SUM(f1) OVER (ORDER BY f2 {sort_order}) FROM t",
+                },
+            )
+
     def test_json_extract(self):
         self.validate_all(
             """SELECT JSON_QUERY('{"class": {"students": []}}', '$.class')""",
diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py
index 13caf1b71f..a9d6330228 100644
--- a/tests/dialects/test_duckdb.py
+++ b/tests/dialects/test_duckdb.py
@@ -79,7 +79,7 @@ def test_duckdb(self):
         self.validate_all(
             "SELECT SUM(X) OVER (ORDER BY x)",
             write={
-                "bigquery": "SELECT SUM(X) OVER (ORDER BY x NULLS LAST)",
+                "bigquery": "SELECT SUM(X) OVER (ORDER BY x)",
                 "duckdb": "SELECT SUM(X) OVER (ORDER BY x)",
                 "mysql": "SELECT SUM(X) OVER (ORDER BY CASE WHEN x IS NULL THEN 1 ELSE 0 END, x)",
             },

Wrong result for transpiling `OVER (ORDER BY .. )` from `trino` to `bigquery`
**Fully reproducible code snippet**
It can be reproduced by the following code:
```python
import sqlglot

sql = """
WITH t1 as (
  select 1 f1, 2 f2 union all select 2 f1, 4 f2 union all select 3 f1, 6 f2
)
select sum(f1) over (order by f2) from t1;
"""
result = sqlglot.transpile(sql, read="trino", write="bigquery")[0]
print(result)
# the output is
# WITH t1 AS (SELECT 1 AS f1, 2 AS f2 UNION ALL SELECT 2 AS f1, 4 AS f2 UNION ALL SELECT 3 AS f1, 6 AS f2)
# SELECT SUM(f1) OVER (ORDER BY f2 NULLS LAST) FROM t1
```
This SQL isn't valid for BigQuery. It will get the error message from BigQuery:
```
NULLS LAST not supported with ascending sort order in RANGE clauses of analytic functions.
```
It seems that `NULLS LAST` shouldn't be added in the `OVER` clause.

If I don't assign the `read` dialect, it works well.
```python
import sqlglot

sql = """
WITH t1 as (
  select 1 f1, 2 f2 union all select 2 f1, 4 f2 union all select 3 f1, 6 f2
)
select sum(f1) over (order by f2) from t1;
"""
result = sqlglot.transpile(sql, read=None, write="bigquery")[0]
print(result)
# WITH t1 AS (SELECT 1 AS f1, 2 AS f2 UNION ALL SELECT 2 AS f1, 4 AS f2 UNION ALL SELECT 3 AS f1, 6 AS f2)
# SELECT SUM(f1) OVER (ORDER BY f2) FROM t1
```

**Official Documentation**
[Window function syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/window-function-calls#syntax)


diff --git a/sqlglot/dialects/dialect.py b/sqlglot/dialects/dialect.py
index 5b12f56c0c..eca2bf592a 100644
--- a/sqlglot/dialects/dialect.py
+++ b/sqlglot/dialects/dialect.py
@@ -411,6 +411,9 @@ class Dialect(metaclass=_Dialect):
     is cast to x's type to match it instead.
     """

+    SUPPORTS_VALUES_DEFAULT = True
+    """Whether the DEFAULT keyword is supported in the VALUES clause."""
+
     REGEXP_EXTRACT_DEFAULT_GROUP = 0
     """The default value for the capturing group."""

diff --git a/sqlglot/dialects/presto.py b/sqlglot/dialects/presto.py
index ec702a097c..056d974c27 100644
--- a/sqlglot/dialects/presto.py
+++ b/sqlglot/dialects/presto.py
@@ -198,6 +198,7 @@ class Presto(Dialect):
     TYPED_DIVISION = True
     TABLESAMPLE_SIZE_IS_PERCENT = True
     LOG_BASE_FIRST: t.Optional[bool] = None
+    SUPPORTS_VALUES_DEFAULT = False

     TIME_MAPPING = MySQL.TIME_MAPPING

diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 2a477d9b90..12419e8c21 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -2946,8 +2946,13 @@ def _parse_partition(self) -> t.Optional[exp.Partition]:
         )

     def _parse_value(self) -> t.Optional[exp.Tuple]:
+        def _parse_value_expression() -> t.Optional[exp.Expression]:
+            if self.dialect.SUPPORTS_VALUES_DEFAULT and self._match(TokenType.DEFAULT):
+                return exp.var(self._prev.text.upper())
+            return self._parse_expression()
+
         if self._match(TokenType.L_PAREN):
-            expressions = self._parse_csv(self._parse_expression)
+            expressions = self._parse_csv(_parse_value_expression)
             self._match_r_paren()
             return self.expression(exp.Tuple, expressions=expressions)

diff --git a/tests/dialects/test_mysql.py b/tests/dialects/test_mysql.py
index fd6b36f1be..9e5b74e0bb 100644
--- a/tests/dialects/test_mysql.py
+++ b/tests/dialects/test_mysql.py
@@ -118,6 +118,13 @@ def test_ddl(self):
             "CREATE TABLE `foo` (a VARCHAR(10), INDEX idx_a (a DESC))",
         )

+        self.validate_all(
+            "insert into t(i) values (default)",
+            write={
+                "duckdb": "INSERT INTO t (i) VALUES (DEFAULT)",
+                "mysql": "INSERT INTO t (i) VALUES (DEFAULT)",
+            },
+        )
         self.validate_all(
             "CREATE TABLE t (id INT UNSIGNED)",
             write={

“Default” is a keyword, but it was converted into a string value after translation.
```
Python 3.13.0 | packaged by Anaconda, Inc. | (main, Oct  7 2024, 16:25:56) [Clang 14.0.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlglot
>>> sqlglot.transpile("insert into t(i) values (default)", read="mysql", write="duckdb")[0]
'INSERT INTO t (i) VALUES ("default")'
```

“Default” is a keyword, but it was converted into a string value after translation.


diff --git a/sqlglot/dialects/bigquery.py b/sqlglot/dialects/bigquery.py
index 5eb02a1492..2f3ac53efb 100644
--- a/sqlglot/dialects/bigquery.py
+++ b/sqlglot/dialects/bigquery.py
@@ -40,6 +40,11 @@
 logger = logging.getLogger("sqlglot")

+JSON_EXTRACT_TYPE = t.Union[exp.JSONExtract, exp.JSONExtractScalar, exp.JSONExtractArray]
+
+DQUOTES_ESCAPING_JSON_FUNCTIONS = ("JSON_QUERY", "JSON_VALUE", "JSON_QUERY_ARRAY")
+
+
 def _derived_table_values_to_unnest(self: BigQuery.Generator, expression: exp.Values) -> str:
     if not expression.find_ancestor(exp.From, exp.Join):
         return self.values_sql(expression)
@@ -324,6 +329,23 @@ def _build_contains_substring(args: t.List) -> exp.Contains | exp.Anonymous:
     return exp.Contains(this=this, expression=expr)

+def _json_extract_sql(self: BigQuery.Generator, expression: JSON_EXTRACT_TYPE) -> str:
+    name = (expression._meta and expression.meta.get("name")) or expression.sql_name()
+    upper = name.upper()
+
+    dquote_escaping = upper in DQUOTES_ESCAPING_JSON_FUNCTIONS
+
+    if dquote_escaping:
+        self._quote_json_path_key_using_brackets = False
+
+    sql = rename_func(upper)(self, expression)
+
+    if dquote_escaping:
+        self._quote_json_path_key_using_brackets = True
+
+    return sql
+
+
 class BigQuery(Dialect):
     WEEK_OFFSET = -1
     UNNEST_COLUMN_ONLY = True
@@ -869,6 +891,9 @@ class Generator(generator.Generator):
             exp.ILike: no_ilike_sql,
             exp.IntDiv: rename_func("DIV"),
             exp.Int64: rename_func("INT64"),
+            exp.JSONExtract: _json_extract_sql,
+            exp.JSONExtractArray: _json_extract_sql,
+            exp.JSONExtractScalar: _json_extract_sql,
             exp.JSONFormat: rename_func("TO_JSON_STRING"),
             exp.Levenshtein: _levenshtein_sql,
             exp.Max: max_or_greatest,
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 5e8e1dccba..842df5a753 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -658,6 +658,7 @@ class Generator(metaclass=_Generator):
         "_next_name",
         "_identifier_start",
         "_identifier_end",
+        "_quote_json_path_key_using_brackets",
     )

     def __init__(
@@ -706,6 +707,8 @@ def __init__(
         self._identifier_start = self.dialect.IDENTIFIER_START
         self._identifier_end = self.dialect.IDENTIFIER_END

+        self._quote_json_path_key_using_brackets = True
+
     def generate(self, expression: exp.Expression, copy: bool = True) -> str:
         """
         Generates the SQL string corresponding to the given syntax tree.
@@ -2871,7 +2874,7 @@ def json_path_part(self, expression: int | str | exp.JSONPathPart) -> str:
         if isinstance(expression, int):
             return str(expression)

-        if self.JSON_PATH_SINGLE_QUOTE_ESCAPE:
+        if self._quote_json_path_key_using_brackets and self.JSON_PATH_SINGLE_QUOTE_ESCAPE:
             escaped = expression.replace("'", "\\'")
             escaped = f"\\'{expression}\\'"
         else:
@@ -4072,7 +4075,11 @@ def _jsonpathkey_sql(self, expression: exp.JSONPathKey) -> str:
             return f".{this}"

         this = self.json_path_part(this)
-        return f"[{this}]" if self.JSON_PATH_BRACKETED_KEY_SUPPORTED else f".{this}"
+        return (
+            f"[{this}]"
+            if self._quote_json_path_key_using_brackets and self.JSON_PATH_BRACKETED_KEY_SUPPORTED
+            else f".{this}"
+        )

     def _jsonpathsubscript_sql(self, expression: exp.JSONPathSubscript) -> str:
         this = self.json_path_part(expression.this)
diff --git a/sqlglot/jsonpath.py b/sqlglot/jsonpath.py
index 911debe4b5..115bd15941 100644
--- a/sqlglot/jsonpath.py
+++ b/sqlglot/jsonpath.py
@@ -146,6 +146,28 @@ def _parse_bracket() -> exp.JSONPathPart:

         return node

+    def _parse_var_text() -> str:
+        """
+        Consumes & returns the text for a var. In BigQuery it's valid to have a key with spaces
+        in it, e.g JSON_QUERY(..., '$. a b c ') should produce a single JSONPathKey(' a b c ').
+        This is done by merging "consecutive" vars until a key separator is found (dot, colon etc)
+        or the path string is exhausted.
+        """
+        prev_index = i - 2
+
+        while _match(TokenType.VAR):
+            pass
+
+        start = 0 if prev_index < 0 else tokens[prev_index].end + 1
+
+        if i >= len(tokens):
+            # This key is the last token for the path, so it's text is the remaining path
+            text = path[start:]
+        else:
+            text = path[start : tokens[i].start]
+
+        return text
+
     # We canonicalize the JSON path AST so that it always starts with a
     # "root" element, so paths like "field" will be generated as "$.field"
     _match(TokenType.DOLLAR)
@@ -155,8 +177,10 @@ def _parse_bracket() -> exp.JSONPathPart:
         if _match(TokenType.DOT) or _match(TokenType.COLON):
             recursive = _prev().text == ".."

-            if _match(TokenType.VAR) or _match(TokenType.IDENTIFIER):
-                value: t.Optional[str | exp.JSONPathWildcard] = _prev().text
+            if _match(TokenType.VAR):
+                value: t.Optional[str | exp.JSONPathWildcard] = _parse_var_text()
+            elif _match(TokenType.IDENTIFIER):
+                value = _prev().text
             elif _match(TokenType.STAR):
                 value = exp.JSONPathWildcard()
             else:
@@ -170,7 +194,9 @@ def _parse_bracket() -> exp.JSONPathPart:
                 raise ParseError(_error("Expected key name or * after DOT"))
         elif _match(TokenType.L_BRACKET):
             expressions.append(_parse_bracket())
-        elif _match(TokenType.VAR) or _match(TokenType.IDENTIFIER):
+        elif _match(TokenType.VAR):
+            expressions.append(exp.JSONPathKey(this=_parse_var_text()))
+        elif _match(TokenType.IDENTIFIER):
             expressions.append(exp.JSONPathKey(this=_prev().text))
         elif _match(TokenType.STAR):
             expressions.append(exp.JSONPathWildcard())

diff --git a/tests/dialects/test_bigquery.py b/tests/dialects/test_bigquery.py
index 366cadedbe..26b12a15fa 100644
--- a/tests/dialects/test_bigquery.py
+++ b/tests/dialects/test_bigquery.py
@@ -1574,14 +1574,6 @@ def test_bigquery(self):
                 "snowflake": "IFF((y) <> 0, (x) / (y), NULL)",
             },
         )
-        self.validate_all(
-            """SELECT JSON_QUERY('{"class": {"students": []}}', '$.class')""",
-            write={
-                "bigquery": """SELECT JSON_QUERY('{"class": {"students": []}}', '$.class')""",
-                "duckdb": """SELECT '{"class": {"students": []}}' -> '$.class'""",
-                "snowflake": """SELECT GET_PATH(PARSE_JSON('{"class": {"students": []}}'), 'class')""",
-            },
-        )
         self.validate_all(
             """SELECT JSON_VALUE_ARRAY('{"arr": [1, "a"]}', '$.arr')""",
             write={
@@ -2139,7 +2131,16 @@ def test_null_ordering(self):
                 },
             )

-    def test_json_extract_scalar(self):
+    def test_json_extract(self):
+        self.validate_all(
+            """SELECT JSON_QUERY('{"class": {"students": []}}', '$.class')""",
+            write={
+                "bigquery": """SELECT JSON_QUERY('{"class": {"students": []}}', '$.class')""",
+                "duckdb": """SELECT '{"class": {"students": []}}' -> '$.class'""",
+                "snowflake": """SELECT GET_PATH(PARSE_JSON('{"class": {"students": []}}'), 'class')""",
+            },
+        )
+
         for func in ("JSON_EXTRACT_SCALAR", "JSON_VALUE"):
             with self.subTest(f"Testing BigQuery's {func}"):
                 self.validate_all(
@@ -2164,6 +2165,18 @@ def test_json_extract_scalar(self):
                     self.parse_one(sql).sql("bigquery", normalize_functions="upper"), sql
                 )

+        # Test double quote escaping
+        for func in ("JSON_VALUE", "JSON_QUERY", "JSON_QUERY_ARRAY"):
+            self.validate_identity(
+                f"{func}(doc, '$. a b c .d')", f"""{func}(doc, '$." a b c ".d')"""
+            )
+
+        # Test single quote & bracket escaping
+        for func in ("JSON_EXTRACT", "JSON_EXTRACT_SCALAR", "JSON_EXTRACT_ARRAY"):
+            self.validate_identity(
+                f"{func}(doc, '$. a b c .d')", f"""{func}(doc, '$[\\' a b c \\'].d')"""
+            )
+
     def test_json_extract_array(self):
         for func in ("JSON_QUERY_ARRAY", "JSON_EXTRACT_ARRAY"):
             with self.subTest(f"Testing BigQuery's {func}"):

bigquery: json_value transpilation gives incorrect result or fails due to escaping
noticed via sqlmesh, which recently updated `sqlglot (25.19.0 -> 25.31.4)`.

We have an expression like `select json_value('{"fu bar": 42}', "$.fu bar")` which runs correctly on bigquery yielding 42.   Previously this was transpiled unchanged, but now results in an incorrect selection (note the added `.`s):

```
>>> sqlglot.__version__
'25.31.4'
>>> print(sqlglot.transpile("""select json_value('{"fu bar": 42}', "$.fu bar")""", read="bigquery", write="bigquery")[0])
SELECT json_value('{"fu bar": 42}', '$.fu.bar')
```
This query now yields `null` instead of 42 in bigquery.

The space-containing-key can be escaped in bigquery with double-quotes for [json_value](https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#json_value) which is apparently different from the bracketed escaping used by [json_extract](https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#json_extract)... 🤯    So this also works successfully in bigquery: `SELECT json_value('{"fu bar": 42}', '$."fu bar"')`.

But it now gets transpiled to sql which throws an error, namely `Invalid token in JSONPath at: ['fu bar']`:

```
>>> print(sqlglot.transpile("""SELECT json_value('{"fu bar": 42}', '$."fu bar"')""", read="bigquery", write="bigquery")[0])
SELECT json_value('{"fu bar": 42}', '$[\'fu bar\']')
```

a workaround is apparently to use the deprecated json_extract[_scalar] functions.  i.e. the query `SELECT json_extract('{"fu bar": 42}', '$[\'fu bar\']')` works correctly and is transpiled unchanged:

```
>>> print(sqlglot.transpile("""SELECT json_extract('{"fu bar": 42}', '$[\\'fu bar\\']')""", read="bigquery", write="bigquery")[0])
SELECT json_extract('{"fu bar": 42}', '$[\'fu bar\']')
```

this is tricky, there are a couple of things we should do @VaggelisD / @georgesittas

1. make it so json tokenizer is sqlglot only, we can do that with a new constant USE_RS_TOKENIZER = USE_RS_TOKENIZER inside the tokenizer
2. Override _scan_var so that it doesn't do a strip() on the peek so it can accept spaces
3. modify _scan so that it doesn't strip white spaces

this is legal in bigquery... json_value('...', '$. a b c '), notice the leading and trailing spaces

we also need to override the escaping rules. for json_value, it uses double quotes to escape, and for json_extract it uses [ to escape.