<commit_before>﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using SqlNotebookScript.Core.AdoModules;
using SqlNotebookScript.Core.GenericModules;
using SqlNotebookScript.Core.SqliteInterop;
using SqlNotebookScript.DataTables;
using SqlNotebookScript.Utils;
using static SqlNotebookScript.Core.SqliteInterop.NativeMethods;

namespace SqlNotebookScript.Core;

public sealed class Notebook : IDisposable {
    public static bool CancelInProgress { get; set; } = false;

    private const int CURRENT_FILE_VERSION = 2;

    private delegate void ExecuteGenericFunctionDelegate(IntPtr a, int b, IntPtr c);

    private static readonly Lazy<(IntPtr Ptr, ExecuteGenericFunctionDelegate Delegate)> _executeGenericFunctionFunc = new(() => {
        ExecuteGenericFunctionDelegate @delegate = ExecuteGenericFunction;
        return (Marshal.GetFunctionPointerForDelegate(@delegate), @delegate);
    });

    private bool _disposedValue;

    private readonly object _lock = new();
    private static readonly object _tokenizeLock = new();

    // Just hang onto these. They are used as unmanaged callbacks.
    private readonly List<object> _delegates = new();

    private List<AdoModuleProvider> _adoModuleProviders =
        new() {
            new MySqlAdoModuleProvider(),
            new PostgreSqlAdoModuleProvider(),
            new SqlServerAdoModuleProvider(),
        };
    private List<GenericModuleProvider> _genericModuleProviders = new();
    public string OriginalFilePath { get; set; }
    private readonly string _workingCopyFilePath;
    private IntPtr _sqlite; // sqlite3*

    public NotebookUserData UserData { get; set; }
    public static string ErrorMessage { get; set; }

    // If the AdoModuleProvider gets an error with an underlying query, there's no way to pass it up through SQLite.
    // So SQLite returns a generic logic error and we store a more specific error message here.
    public static string SqliteVtabErrorMessage { get; set; }

    public static Notebook New() =>
        new(null, true, null, CancellationToken.None);

    public static Notebook Open(string filePath, Action<int> onPercentComplete = null, CancellationToken cancel = default) =>
        new(filePath, false, onPercentComplete, cancel);

    private Notebook(string filePath, bool isNew, Action<int> onPercentComplete, CancellationToken cancel) {
        _workingCopyFilePath = NotebookTempFiles.GetTempFilePath(".working-copy");
        if (!isNew) {
            try {
                CopyFile(filePath, _workingCopyFilePath, onPercentComplete, cancel);
            } catch {
                File.Delete(_workingCopyFilePath);
                throw;
            }
        }

        OriginalFilePath = filePath;
        Invoke(Init);
    }

    private void Dispose(bool disposing) {
        if (!_disposedValue) {
            if (disposing) {
                // dispose managed state (managed objects)
            }

            // free unmanaged resources (unmanaged objects) and override finalizer
            // set large fields to null
            if (_sqlite != IntPtr.Zero) {
                var result = sqlite3_close(_sqlite);
                Debug.Assert(result == SQLITE_OK);
                _sqlite = IntPtr.Zero;
            }
            
            foreach (var x in _adoModuleProviders) {
                x.Dispose();
            }
            _adoModuleProviders = null;

            foreach (var x in _genericModuleProviders) {
                x.Dispose();
            }
            _genericModuleProviders = null;

            UserData = null;

            _disposedValue = true;
        }
    }

    ~Notebook() {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: false);
    }

    public void Dispose() {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    private readonly record struct CustomFunctions(
        List<CustomTableFunction> CustomTableFunctions, List<CustomScalarFunction> CustomScalarFunctions);

    private static CustomFunctions FindCustomFunctions() {
        var customTableFunctionType = typeof(CustomTableFunction);
        var customScalarFunctionType = typeof(CustomScalarFunction);

        List<CustomTableFunction> customTableFunctions = new();
        List<CustomScalarFunction> customScalarFunctions = new();

        foreach (var type in typeof(CustomTableFunction).Assembly.GetExportedTypes()) {
            if (!type.IsAbstract) {
                if (type.IsAssignableTo(customTableFunctionType)) {
                    customTableFunctions.Add((CustomTableFunction)Activator.CreateInstance(type));
                } else if (type.IsAssignableTo(customScalarFunctionType)) {
                    customScalarFunctions.Add((CustomScalarFunction)Activator.CreateInstance(type));
                }
            }
        }

        return new(customTableFunctions, customScalarFunctions);
    }

    private void Init() {
        FileVersionMigrator.MigrateIfNeeded(_workingCopyFilePath);
            
        using NativeString filePathNative = new(_workingCopyFilePath);

        using NativeBuffer sqliteNative = new(IntPtr.Size);

        SqliteUtil.ThrowIfError(IntPtr.Zero,
            sqlite3_open(filePathNative.Ptr, sqliteNative.Ptr));
        _sqlite = Marshal.ReadIntPtr(sqliteNative.Ptr); // sqlite3*

        foreach (var x in _adoModuleProviders) {
            x.Install(_sqlite);
        }

        var (tableFunctions, scalarFunctions) = FindCustomFunctions();
        foreach (var tableFunction in tableFunctions) {<commit_msg>Fix linked tables broken after save<commit_after>﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using SqlNotebookScript.Core.AdoModules;
using SqlNotebookScript.Core.GenericModules;
using SqlNotebookScript.Core.SqliteInterop;
using SqlNotebookScript.DataTables;
using SqlNotebookScript.Utils;
using static SqlNotebookScript.Core.SqliteInterop.NativeMethods;

namespace SqlNotebookScript.Core;

public sealed class Notebook : IDisposable {
    public static bool CancelInProgress { get; set; } = false;

    private const int CURRENT_FILE_VERSION = 2;

    private delegate void ExecuteGenericFunctionDelegate(IntPtr a, int b, IntPtr c);

    private static readonly Lazy<(IntPtr Ptr, ExecuteGenericFunctionDelegate Delegate)> _executeGenericFunctionFunc = new(() => {
        ExecuteGenericFunctionDelegate @delegate = ExecuteGenericFunction;
        return (Marshal.GetFunctionPointerForDelegate(@delegate), @delegate);
    });

    private bool _disposedValue;

    private readonly object _lock = new();
    private static readonly object _tokenizeLock = new();

    // Just hang onto these. They are used as unmanaged callbacks.
    private readonly List<object> _delegates = new();

    private List<AdoModuleProvider> _adoModuleProviders = new();
    private List<GenericModuleProvider> _genericModuleProviders = new();
    public string OriginalFilePath { get; set; }
    private readonly string _workingCopyFilePath;
    private IntPtr _sqlite; // sqlite3*

    public NotebookUserData UserData { get; set; }
    public static string ErrorMessage { get; set; }

    // If the AdoModuleProvider gets an error with an underlying query, there's no way to pass it up through SQLite.
    // So SQLite returns a generic logic error and we store a more specific error message here.
    public static string SqliteVtabErrorMessage { get; set; }

    public static Notebook New() =>
        new(null, true, null, CancellationToken.None);

    public static Notebook Open(string filePath, Action<int> onPercentComplete = null, CancellationToken cancel = default) =>
        new(filePath, false, onPercentComplete, cancel);

    private Notebook(string filePath, bool isNew, Action<int> onPercentComplete, CancellationToken cancel) {
        _workingCopyFilePath = NotebookTempFiles.GetTempFilePath(".working-copy");
        if (!isNew) {
            try {
                CopyFile(filePath, _workingCopyFilePath, onPercentComplete, cancel);
            } catch {
                File.Delete(_workingCopyFilePath);
                throw;
            }
        }

        OriginalFilePath = filePath;
        Invoke(Init);
    }

    private void Dispose(bool disposing) {
        if (!_disposedValue) {
            if (disposing) {
                // dispose managed state (managed objects)
            }

            // free unmanaged resources (unmanaged objects) and override finalizer
            // set large fields to null
            if (_sqlite != IntPtr.Zero) {
                var result = sqlite3_close(_sqlite);
                Debug.Assert(result == SQLITE_OK);
                _sqlite = IntPtr.Zero;
            }
            
            foreach (var x in _adoModuleProviders) {
                x.Dispose();
            }
            _adoModuleProviders = null;

            foreach (var x in _genericModuleProviders) {
                x.Dispose();
            }
            _genericModuleProviders = null;

            UserData = null;

            _disposedValue = true;
        }
    }

    ~Notebook() {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: false);
    }

    public void Dispose() {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    private readonly record struct CustomFunctions(
        List<CustomTableFunction> CustomTableFunctions, List<CustomScalarFunction> CustomScalarFunctions);

    private static CustomFunctions FindCustomFunctions() {
        var customTableFunctionType = typeof(CustomTableFunction);
        var customScalarFunctionType = typeof(CustomScalarFunction);

        List<CustomTableFunction> customTableFunctions = new();
        List<CustomScalarFunction> customScalarFunctions = new();

        foreach (var type in typeof(CustomTableFunction).Assembly.GetExportedTypes()) {
            if (!type.IsAbstract) {
                if (type.IsAssignableTo(customTableFunctionType)) {
                    customTableFunctions.Add((CustomTableFunction)Activator.CreateInstance(type));
                } else if (type.IsAssignableTo(customScalarFunctionType)) {
                    customScalarFunctions.Add((CustomScalarFunction)Activator.CreateInstance(type));
                }
            }
        }

        return new(customTableFunctions, customScalarFunctions);
    }

    private void Init() {
        FileVersionMigrator.MigrateIfNeeded(_workingCopyFilePath);
            
        using NativeString filePathNative = new(_workingCopyFilePath);

        using NativeBuffer sqliteNative = new(IntPtr.Size);

        SqliteUtil.ThrowIfError(IntPtr.Zero,
            sqlite3_open(filePathNative.Ptr, sqliteNative.Ptr));
        _sqlite = Marshal.ReadIntPtr(sqliteNative.Ptr); // sqlite3*

        foreach (var x in _adoModuleProviders) {
            x.Dispose();
        }
        _adoModuleProviders.Clear();
        _adoModuleProviders.Add(new MySqlAdoModuleProvider());
        _adoModuleProviders.Add(new PostgreSqlAdoModuleProvider());
        _adoModuleProviders.Add(new SqlServerAdoModuleProvider());
        foreach (var x in _adoModuleProviders) {
            x.Install(_sqlite);
        }

        var (tableFunctions, scalarFunctions) = FindCustomFunctions();
        foreach (var tableFunction in tableFunctions) {
{'fraction_non_alphanumeric': 0.07998317914213625, 'fraction_numerical': 0.0008410428931875525, 'mean_word_length': 2.777318932655654, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '396429', 'n_tokens_mistral': 3058, 'n_tokens_neox': 2969, 'n_words': 798}