diff --git a/feed.atom b/feed.atom index ca16000..29ea9e7 100644 --- a/feed.atom +++ b/feed.atom @@ -4,8 +4,465 @@ Eric Winnington 2024 - 2024-07-18T11:23:36Z + 2024-10-05T08:57:06Z A collection of thoughts, code and snippets. + + http://ewinnington.github.io/posts/Audit-Trail-Oracle + Using an audit trail table on Oracle + + 2024-10-05T08:00:00Z + <h1 id="implementing-auditable-updates-in-a-relational-database">Implementing Auditable Updates in a Relational Database</h1> +<p>In modern applications, maintaining an audit trail of changes to data is crucial for compliance, debugging, and data integrity. This blog post explores a straightforward approach to implementing auditable updates in a relational database system, specifically focusing on a project management scenario with hierarchical data.</p> +<h2 id="problem-description">Problem Description</h2> +<p>We have a relational database containing <code>Projects</code>, each of which includes <code>Instruments</code>, <code>Markets</code>, and <code>Valuations</code>. These entities form a tree structure, adhering to the third normal form (3NF). Previously, any update to a project involved downloading the entire project tree, making changes, and uploading a new project under a new ID to ensure complete auditability.</p> +<p>This approach is inefficient for small updates and doesn't allow for granular tracking of changes. The goal is to enable small, precise updates to projects while maintaining a comprehensive audit trail of all changes.</p> +<h2 id="solution-overview">Solution Overview</h2> +<p>We introduce an audit table that records every change made to the database. The audit table will store serialized JSON representations of operations like <code>update</code>, <code>insert</code>, and <code>delete</code>. We'll also provide C# code to apply and revert these changes, effectively creating an undo stack.</p> +<p>Let's use the following DB Schema for illustration:</p> +<p><img src="/posts/images/audit-trail/TableStructureBlog.png" class="img-fluid" alt="TableVide" /></p> +<ul> +<li>Primary Keys: Each table has a primary key (e.g., <code>ProjectID</code>, <code>InstrumentID</code>).</li> +<li>Foreign Keys: Child tables reference their parent via foreign keys (e.g., <code>instruments.ProjectID</code> references <code>projects.ProjectID</code>).</li> +<li>Audit Table: The <code>change_audit</code> table records changes with fields like <code>ChangeAuditID</code>, <code>TimeApplied</code>, and <code>ImpactJson</code>.</li> +</ul> +<!-- +```d2 +projects: { + shape: sql_table + ProjectID: int {constraint: primary_key} + Name: varchar(100) + Description: text + LastUpdated: timestamp with time zone + VersionNumber: int +} + +instruments: { + shape: sql_table + InstrumentID: int {constraint: primary_key} + ProjectID: int {constraint: foreign_key} + Name: varchar(100) + Type: varchar(50) + LastUpdated: timestamp with time zone +} + +markets: { + shape: sql_table + MarketID: int {constraint: primary_key} + ProjectID: int {constraint: foreign_key} + Region: varchar(50) + MarketType: varchar(50) + LastUpdated: timestamp with time zone +} + +valuations: { + shape: sql_table + ValuationID: int {constraint: primary_key} + ProjectID: int {constraint: foreign_key} + Value: decimal(10, 2) + Currency: varchar(10) + LastUpdated: timestamp with time zone +} + +change_audit: { + shape: sql_table + ChangeAuditID: int {constraint: primary_key} + TimeApplied: timestamp with time zone + UserID: varchar(100) + ImpactJson: jsonb +} + +instruments.ProjectID -> projects.ProjectID +markets.ProjectID -> projects.ProjectID +valuations.ProjectID -> projects.ProjectID + +``` +--> +<h2 id="implementing-change-auditing">Implementing Change Auditing</h2> +<p>The <code>change_audit</code> table is designed to store all changes in a JSON format for flexibility and ease of storage.</p> +<pre><code class="language-sql">CREATE TABLE change_audit ( + ChangeAuditID NUMBER PRIMARY KEY, + TimeApplied TIMESTAMP, + UserID VARCHAR2(100), + ImpactJson CLOB +); +</code></pre> +<h2 id="json-structure-for-changes">JSON Structure for Changes</h2> +<p>Each change is recorded as a JSON object:</p> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;update&quot;, + &quot;impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: {&quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 2}, + &quot;Column&quot;: &quot;Name&quot;, + &quot;OldValue&quot;: &quot;Old Instrument Name&quot;, + &quot;NewValue&quot;: &quot;Updated Instrument Name&quot; + } + ] +} +</code></pre> +<h2 id="csharp-to-apply-changes-given-an-operation">CSharp to apply changes given an operation</h2> +<p>To apply changes recorded in the JSON, we'll use C# code that parses the JSON and executes the corresponding SQL commands.</p> +<p>I assume you have the <code>_connectionString</code> available somewhere as a constant in the code.</p> +<pre><code class="language-csharp">using Oracle.ManagedDataAccess.Client; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +public class ChangeApplier +{ + + public void ApplyChanges(string jsonInput) + { + // Parse the JSON input + var operation = JObject.Parse(jsonInput); + string opType = operation[&quot;Operation&quot;].ToString(); + var impactList = (JArray)operation[&quot;impact&quot;]; + + using (var conn = new OracleConnection(_connectionString)) + { + conn.Open(); + using (var transaction = conn.BeginTransaction()) + { + try + { + foreach (var impact in impactList) + { + string table = impact[&quot;Table&quot;].ToString(); + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + string column = impact[&quot;Column&quot;]?.ToString(); + string newValue = impact[&quot;NewValue&quot;]?.ToString(); + + switch (opType) + { + case &quot;update&quot;: + ApplyUpdate(conn, table, primaryKey, column, newValue); + break; + case &quot;insert&quot;: + ApplyInsert(conn, table, impact); + break; + case &quot;delete&quot;: + ApplyDelete(conn, table, primaryKey); + break; + } + } + + transaction.Commit(); + } + catch (Exception ex) + { + transaction.Rollback(); + Console.WriteLine($&quot;Error applying changes: {ex.Message}&quot;); + } + } + } + } + + private void ApplyUpdate(OracleConnection conn, string table, JObject primaryKey, string column, string newValue) + { + var pkConditions = BuildPrimaryKeyCondition(primaryKey); + var query = $&quot;UPDATE {table} SET {column} = :newValue WHERE {pkConditions}&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + cmd.Parameters.Add(new OracleParameter(&quot;newValue&quot;, newValue)); + cmd.ExecuteNonQuery(); + } + } + + private void ApplyInsert(OracleConnection conn, string table, JToken impact) + { + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + var newValues = (JObject)impact[&quot;NewValues&quot;]; + var columns = new List&lt;string&gt;(); + var values = new List&lt;string&gt;(); + + foreach (var property in primaryKey.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + foreach (var property in newValues.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + var query = $&quot;INSERT INTO {table} ({string.Join(&quot;, &quot;, columns)}) VALUES ({string.Join(&quot;, &quot;, values)})&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + foreach (var property in primaryKey.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + foreach (var property in newValues.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + cmd.ExecuteNonQuery(); + } + } + + private void ApplyDelete(OracleConnection conn, string table, JObject primaryKey) + { + var pkConditions = BuildPrimaryKeyCondition(primaryKey); + var query = $&quot;DELETE FROM {table} WHERE {pkConditions}&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + cmd.ExecuteNonQuery(); + } + } + + private string BuildPrimaryKeyCondition(JObject primaryKey) + { + var conditions = new List&lt;string&gt;(); + foreach (var prop in primaryKey.Properties()) + { + conditions.Add($&quot;{prop.Name} = :{prop.Name}&quot;); + } + return string.Join(&quot; AND &quot;, conditions); + } +} +</code></pre> +<ul> +<li><strong>ApplyChanges</strong>: Parses the JSON input and determines the operation type.</li> +<li><strong>ApplyUpdate</strong>: Executes an UPDATE SQL command using parameters to prevent SQL injection.</li> +<li><strong>ApplyInsert</strong>: Executes an INSERT SQL command, constructing columns and values from the JSON.</li> +<li><strong>ApplyDelete</strong>: Executes a DELETE SQL command based on the primary key. +BuildPrimaryKeyCondition: Constructs the WHERE clause for SQL commands.</li> +</ul> +<p>A side note, for the insert, you'll have the challenge if you are using auto-incremented IDs, this will mean you don't know the new IDs until you have inserted the data, so you should make sure to capture the new IDs and then create the audit log. This is left as a simple exercise to the reader in case it is necessary.</p> +<h2 id="csharp-to-revert-changes">CSharp to revert changes</h2> +<p>To revert changes (undo operations), we'll process the audit trail in reverse order. Here I give the processing of a list of operations as an example of unrolling. It is to note that the reverse delete does only one table, so if there was some connected information that was deleted via referential identity, it was the task of the audit table to keep that in the audit.</p> +<pre><code class="language-csharp">public class ChangeReverter +{ + public void RevertChanges(List&lt;string&gt; jsonOperations) + { + using (var conn = new OracleConnection(_connectionString)) + { + conn.Open(); + using (var transaction = conn.BeginTransaction()) + { + try + { + jsonOperations.Reverse(); // note: you could also have provided sorted by last time from the audit table instead of reversing them + + foreach (var operationJson in jsonOperations) + { + var operation = JObject.Parse(operationJson); + string opType = operation[&quot;Operation&quot;].ToString(); + var impactList = (JArray)operation[&quot;impact&quot;]; + + foreach (var impact in impactList) + { + string table = impact[&quot;Table&quot;].ToString(); + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + string column = impact[&quot;Column&quot;]?.ToString(); + string oldValue = impact[&quot;OldValue&quot;]?.ToString(); + + switch (opType) + { + case &quot;update&quot;: + RevertUpdate(conn, table, primaryKey, column, oldValue); + break; + case &quot;insert&quot;: + ApplyDelete(conn, table, primaryKey); + break; + case &quot;delete&quot;: + RevertDelete(conn, table, impact); + break; + } + } + } + + transaction.Commit(); + } + catch (Exception ex) + { + transaction.Rollback(); + Console.WriteLine($&quot;Error reverting changes: {ex.Message}&quot;); + } + } + } + } + + private void RevertUpdate(OracleConnection conn, string table, JObject primaryKey, string column, string oldValue) + { + var pkConditions = BuildPrimaryKeyCondition(primaryKey); + var query = $&quot;UPDATE {table} SET {column} = :oldValue WHERE {pkConditions}&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + cmd.Parameters.Add(new OracleParameter(&quot;oldValue&quot;, oldValue)); + cmd.ExecuteNonQuery(); + } + } + + private void RevertDelete(OracleConnection conn, string table, JToken impact) + { + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + var oldValues = (JObject)impact[&quot;OldValues&quot;]; + var columns = new List&lt;string&gt;(); + var values = new List&lt;string&gt;(); + + foreach (var property in primaryKey.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + foreach (var property in oldValues.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + var query = $&quot;INSERT INTO {table} ({string.Join(&quot;, &quot;, columns)}) VALUES ({string.Join(&quot;, &quot;, values)})&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + foreach (var property in primaryKey.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + foreach (var property in oldValues.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + cmd.ExecuteNonQuery(); + } + } + + // Reuse BuildPrimaryKeyCondition and ApplyDelete methods from ChangeApplier +} +</code></pre> +<ul> +<li><strong>RevertChanges</strong>: Processes the list of JSON operations in reverse order to undo changes.</li> +<li><strong>RevertUpdate</strong>: Sets the column back to its old value.</li> +<li><strong>RevertDelete</strong>: Re-inserts a deleted row using the old values stored in the audit trail.</li> +<li><strong>ApplyDelete</strong>: Deletes a row, used here to undo an insert operation.</li> +</ul> +<h2 id="json-schema">JSON schema</h2> +<p>The reason that I prefer to use the Json directly in the C# code is that actually making up the C# classes for this schema is actually more work that processing the json directly in the code.</p> +<pre><code class="language-json">{ + &quot;$schema&quot;: &quot;http://json-schema.org/draft-07/schema#&quot;, + &quot;title&quot;: &quot;ImpactJsonRoot&quot;, + &quot;type&quot;: &quot;object&quot;, + &quot;properties&quot;: { + &quot;Operation&quot;: { + &quot;type&quot;: &quot;string&quot;, + &quot;enum&quot;: [&quot;update&quot;, &quot;insert&quot;, &quot;delete&quot;], + &quot;description&quot;: &quot;Type of operation&quot; + }, + &quot;Impact&quot;: { + &quot;type&quot;: &quot;array&quot;, + &quot;items&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;properties&quot;: { + &quot;Table&quot;: { + &quot;type&quot;: &quot;string&quot;, + &quot;description&quot;: &quot;Name of the table affected&quot; + }, + &quot;PrimaryKey&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;description&quot;: &quot;Primary key fields and their values&quot;, + &quot;additionalProperties&quot;: { + &quot;type&quot;: [&quot;number&quot;, &quot;null&quot;] + } + }, + &quot;Column&quot;: { + &quot;type&quot;: &quot;string&quot;, + &quot;description&quot;: &quot;Column affected (for updates)&quot; + }, + &quot;OldValue&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;], + &quot;description&quot;: &quot;Previous value (for updates and deletes)&quot; + }, + &quot;NewValue&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;], + &quot;description&quot;: &quot;New value (for updates and inserts)&quot; + }, + &quot;OldValues&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;description&quot;: &quot;All old values (for deletes)&quot;, + &quot;additionalProperties&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;] + } + }, + &quot;NewValues&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;description&quot;: &quot;All new values (for inserts)&quot;, + &quot;additionalProperties&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;] + } + } + }, + &quot;required&quot;: [&quot;Table&quot;, &quot;PrimaryKey&quot;] + } + } + }, + &quot;required&quot;: [&quot;Operation&quot;, &quot;Impact&quot;] +} +</code></pre> +<p>and here are examples of operations:</p> +<h3 id="update">update</h3> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;update&quot;, + &quot;Impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: { &quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 2 }, + &quot;Column&quot;: &quot;Name&quot;, + &quot;OldValue&quot;: &quot;Old Instrument Name&quot;, + &quot;NewValue&quot;: &quot;Updated Instrument Name&quot; + } + ] +} + +</code></pre> +<h3 id="insert">insert</h3> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;insert&quot;, + &quot;Impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: { &quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 10 }, + &quot;NewValues&quot;: { + &quot;Name&quot;: &quot;New Instrument&quot;, + &quot;Type&quot;: &quot;Flexible Asset&quot;, + &quot;LastUpdated&quot;: &quot;2024-10-05T12:34:56Z&quot; + } + } + ] +} +</code></pre> +<h3 id="delete">delete</h3> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;delete&quot;, + &quot;Impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: { &quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 5 }, + &quot;OldValues&quot;: { + &quot;Name&quot;: &quot;Obsolete Instrument&quot;, + &quot;Type&quot;: &quot;Flexible Asset&quot;, + &quot;LastUpdated&quot;: &quot;2024-10-01T09:15:00Z&quot; + } + } + ] +} +</code></pre> +<p>Note: OpenAI's <code>o1-preview</code> was used to assist in the creation of the post.</p> + + <p>In modern applications, maintaining an audit trail of changes to data is crucial for compliance, debugging, and data integrity. This blog post explores a straightforward approach to implementing auditable updates in a relational database system, specifically focusing on a project management scenario with hierarchical data.</p> + http://ewinnington.github.io/posts/snippets-in-vscode VSCode Snippets @@ -1399,107 +1856,4 @@ SolveAndPrint(milp_solver, nItems, weights); <p>Thanks to the integration of C# into <a href="https://jupyter.org/">Jupyter notebooks</a> with the <a href="https://github.com/dotnet/try">kernel from Donet try</a> and support from the <a href="https://mybinder.org/">MyBinder.org</a> hosting, it's easy to share with you runnable workbooks to illustrate how to use the <a href="https://developers.google.com/optimization">Google OR-Tools</a> to solve the <a href="https://en.wikipedia.org/wiki/Linear_programming">linear</a> (LP) and <a href="https://en.wikipedia.org/wiki/Integer_programming">mixed-integer linear problems</a> (MILP) .</p> - - http://ewinnington.github.io/posts/my-binder-jupyter-csharp - Hosting your C# Jupyter notebook online by adding one file to your repo - - 2019-11-14T23:20:00Z - <p><a href="https://mybinder.org/">MyBinder.org</a> in collaboration with <a href="https://github.com/dotnet/try">Dotnet try</a> allows you to host your .net notebooks online.</p> -<p><a href="https://mybinder.org/v2/gh/ewinnington/noteb/master?filepath=SqliteInteraction.ipynb">SQLite example workbook: </a> -<a href="https://mybinder.org/v2/gh/ewinnington/noteb/master?filepath=SqliteInteraction.ipynb"><img src="https://mybinder.org/badge_logo.svg" class="img-fluid" alt="Binder" /></a></p> -<p>To light up this for your own hosted repositories, you will need a public github repo. Inside the repository, you will need to create a <a href="https://www.docker.com/">Docker</a> file that gives the setup required for MyBinder to setup the environment of the workbook.</p> -<p>The <a href="https://github.com/dotnet/try/blob/master/CreateBinder.md">dotnet/try</a> has the set of instrunctions.</p> -<p>For my repository, I used the following <a href="https://github.com/ewinnington/noteb/blob/master/Dockerfile">Dockerfile</a></p> -<p>A list of my changes to the standard one proposed by dotnet/try:</p> -<ul> -<li>I used a fixed docker image <code>jupyter/scipy-notebook:45f07a14b422</code></li> -<li>Since I have all my notebooks in the root of my repository I did <code>COPY . ${HOME}/Notebooks/</code></li> -<li>Since I am always importing the Nuget files at the top of my workbook, I did not need to have the docker deamon add a nuget config. So I commented out the COPY command <code># COPY ./NuGet.config ${HOME}/nuget.config</code></li> -<li>I commented out the custom <code>--add-source &quot;https://dotnet.myget.org/F/dotnet-try/api/v3/index.json&quot;</code> from the installation of the dotnet try tool, since I had issue with the nuget feed with the pre-release version. Installing with <code>RUN dotnet tool install -g dotnet-try</code> will get you the latest released version.</li> -</ul> -<pre><code class="language-Skip">FROM jupyter/scipy-notebook:45f07a14b422 - -# Install .NET CLI dependencies - -ARG NB_USER=jovyan -ARG NB_UID=1000 -ENV USER ${NB_USER} -ENV NB_UID ${NB_UID} -ENV HOME /home/${NB_USER} - -WORKDIR ${HOME} - -USER root -RUN apt-get update -RUN apt-get install -y curl - -# Install .NET CLI dependencies -RUN apt-get install -y --no-install-recommends \ - libc6 \ - libgcc1 \ - libgssapi-krb5-2 \ - libicu60 \ - libssl1.1 \ - libstdc++6 \ - zlib1g - -RUN rm -rf /var/lib/apt/lists/* - -# Install .NET Core SDK -ENV DOTNET_SDK_VERSION 3.0.100 - -RUN curl -SL --output dotnet.tar.gz https://dotnetcli.blob.core.windows.net/dotnet/Sdk/$DOTNET_SDK_VERSION/dotnet-sdk-$DOTNET_SDK_VERSION-linux-x64.tar.gz \ - &amp;&amp; dotnet_sha512='766da31f9a0bcfbf0f12c91ea68354eb509ac2111879d55b656f19299c6ea1c005d31460dac7c2a4ef82b3edfea30232c82ba301fb52c0ff268d3e3a1b73d8f7' \ - &amp;&amp; echo &quot;$dotnet_sha512 dotnet.tar.gz&quot; | sha512sum -c - \ - &amp;&amp; mkdir -p /usr/share/dotnet \ - &amp;&amp; tar -zxf dotnet.tar.gz -C /usr/share/dotnet \ - &amp;&amp; rm dotnet.tar.gz \ - &amp;&amp; ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet - -# Enable detection of running in a container -ENV DOTNET_RUNNING_IN_CONTAINER=true \ - # Enable correct mode for dotnet watch (only mode supported in a container) - DOTNET_USE_POLLING_FILE_WATCHER=true \ - # Skip extraction of XML docs - generally not useful within an image/container - helps performance - NUGET_XMLDOC_MODE=skip \ - # Opt out of telemetry until after we install jupyter when building the image, this prevents caching of machine id - DOTNET_TRY_CLI_TELEMETRY_OPTOUT=true - -# Trigger first run experience by running arbitrary cmd -RUN dotnet help - -# Copy notebooks - -COPY . ${HOME}/Notebooks/ - -# Copy package sources - -# COPY ./NuGet.config ${HOME}/nuget.config - -RUN chown -R ${NB_UID} ${HOME} -USER ${USER} - -# Install Microsoft.DotNet.Interactive -RUN dotnet tool install -g dotnet-try -#--add-source &quot;https://dotnet.myget.org/F/dotnet-try/api/v3/index.json&quot; - -ENV PATH=&quot;${PATH}:${HOME}/.dotnet/tools&quot; -RUN echo &quot;$PATH&quot; - -# Install kernel specs -RUN dotnet try jupyter install - -# Enable telemetry once we install jupyter for the image -ENV DOTNET_TRY_CLI_TELEMETRY_OPTOUT=false - -# Set root to Notebooks -WORKDIR ${HOME}/Notebooks/ -</code></pre> -<p>Once the Dockerfile is in the repository. Head over to <a href="https://mybinder.org/">MyBinder.org</a> and enter the link to your repository. Optionally, you can set an initial ipynb file to start when the link is clicked.</p> -<p><img src="/posts/images/my-binder/Binder-1.png" class="img-fluid" alt="MyBinder" /></p> -<p>When you click &quot;launch&quot;, MyBinder will download your repository and start the docker build, very soon you will be able to access your binders online. Fully shareable and totally awesome!</p> -<p><img src="/posts/images/my-binder/Binder-2.png" class="img-fluid" width="60%" alt="SQLite Running" /></p> - - <p><a href="https://mybinder.org/">MyBinder.org</a> in collaboration with <a href="https://github.com/dotnet/try">Dotnet try</a> allows you to host your .net notebooks online.</p> - \ No newline at end of file diff --git a/feed.rss b/feed.rss index bae570a..ae0ea1d 100644 --- a/feed.rss +++ b/feed.rss @@ -5,8 +5,465 @@ http://ewinnington.github.io/ A collection of thoughts, code and snippets. 2024 - Thu, 18 Jul 2024 11:23:36 GMT - Thu, 18 Jul 2024 11:23:36 GMT + Sat, 05 Oct 2024 08:57:06 GMT + Sat, 05 Oct 2024 08:57:06 GMT + + Using an audit trail table on Oracle + http://ewinnington.github.io/posts/Audit-Trail-Oracle + <p>In modern applications, maintaining an audit trail of changes to data is crucial for compliance, debugging, and data integrity. This blog post explores a straightforward approach to implementing auditable updates in a relational database system, specifically focusing on a project management scenario with hierarchical data.</p> + http://ewinnington.github.io/posts/Audit-Trail-Oracle + Sat, 05 Oct 2024 08:00:00 GMT + <h1 id="implementing-auditable-updates-in-a-relational-database">Implementing Auditable Updates in a Relational Database</h1> +<p>In modern applications, maintaining an audit trail of changes to data is crucial for compliance, debugging, and data integrity. This blog post explores a straightforward approach to implementing auditable updates in a relational database system, specifically focusing on a project management scenario with hierarchical data.</p> +<h2 id="problem-description">Problem Description</h2> +<p>We have a relational database containing <code>Projects</code>, each of which includes <code>Instruments</code>, <code>Markets</code>, and <code>Valuations</code>. These entities form a tree structure, adhering to the third normal form (3NF). Previously, any update to a project involved downloading the entire project tree, making changes, and uploading a new project under a new ID to ensure complete auditability.</p> +<p>This approach is inefficient for small updates and doesn't allow for granular tracking of changes. The goal is to enable small, precise updates to projects while maintaining a comprehensive audit trail of all changes.</p> +<h2 id="solution-overview">Solution Overview</h2> +<p>We introduce an audit table that records every change made to the database. The audit table will store serialized JSON representations of operations like <code>update</code>, <code>insert</code>, and <code>delete</code>. We'll also provide C# code to apply and revert these changes, effectively creating an undo stack.</p> +<p>Let's use the following DB Schema for illustration:</p> +<p><img src="/posts/images/audit-trail/TableStructureBlog.png" class="img-fluid" alt="TableVide" /></p> +<ul> +<li>Primary Keys: Each table has a primary key (e.g., <code>ProjectID</code>, <code>InstrumentID</code>).</li> +<li>Foreign Keys: Child tables reference their parent via foreign keys (e.g., <code>instruments.ProjectID</code> references <code>projects.ProjectID</code>).</li> +<li>Audit Table: The <code>change_audit</code> table records changes with fields like <code>ChangeAuditID</code>, <code>TimeApplied</code>, and <code>ImpactJson</code>.</li> +</ul> +<!-- +```d2 +projects: { + shape: sql_table + ProjectID: int {constraint: primary_key} + Name: varchar(100) + Description: text + LastUpdated: timestamp with time zone + VersionNumber: int +} + +instruments: { + shape: sql_table + InstrumentID: int {constraint: primary_key} + ProjectID: int {constraint: foreign_key} + Name: varchar(100) + Type: varchar(50) + LastUpdated: timestamp with time zone +} + +markets: { + shape: sql_table + MarketID: int {constraint: primary_key} + ProjectID: int {constraint: foreign_key} + Region: varchar(50) + MarketType: varchar(50) + LastUpdated: timestamp with time zone +} + +valuations: { + shape: sql_table + ValuationID: int {constraint: primary_key} + ProjectID: int {constraint: foreign_key} + Value: decimal(10, 2) + Currency: varchar(10) + LastUpdated: timestamp with time zone +} + +change_audit: { + shape: sql_table + ChangeAuditID: int {constraint: primary_key} + TimeApplied: timestamp with time zone + UserID: varchar(100) + ImpactJson: jsonb +} + +instruments.ProjectID -> projects.ProjectID +markets.ProjectID -> projects.ProjectID +valuations.ProjectID -> projects.ProjectID + +``` +--> +<h2 id="implementing-change-auditing">Implementing Change Auditing</h2> +<p>The <code>change_audit</code> table is designed to store all changes in a JSON format for flexibility and ease of storage.</p> +<pre><code class="language-sql">CREATE TABLE change_audit ( + ChangeAuditID NUMBER PRIMARY KEY, + TimeApplied TIMESTAMP, + UserID VARCHAR2(100), + ImpactJson CLOB +); +</code></pre> +<h2 id="json-structure-for-changes">JSON Structure for Changes</h2> +<p>Each change is recorded as a JSON object:</p> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;update&quot;, + &quot;impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: {&quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 2}, + &quot;Column&quot;: &quot;Name&quot;, + &quot;OldValue&quot;: &quot;Old Instrument Name&quot;, + &quot;NewValue&quot;: &quot;Updated Instrument Name&quot; + } + ] +} +</code></pre> +<h2 id="csharp-to-apply-changes-given-an-operation">CSharp to apply changes given an operation</h2> +<p>To apply changes recorded in the JSON, we'll use C# code that parses the JSON and executes the corresponding SQL commands.</p> +<p>I assume you have the <code>_connectionString</code> available somewhere as a constant in the code.</p> +<pre><code class="language-csharp">using Oracle.ManagedDataAccess.Client; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +public class ChangeApplier +{ + + public void ApplyChanges(string jsonInput) + { + // Parse the JSON input + var operation = JObject.Parse(jsonInput); + string opType = operation[&quot;Operation&quot;].ToString(); + var impactList = (JArray)operation[&quot;impact&quot;]; + + using (var conn = new OracleConnection(_connectionString)) + { + conn.Open(); + using (var transaction = conn.BeginTransaction()) + { + try + { + foreach (var impact in impactList) + { + string table = impact[&quot;Table&quot;].ToString(); + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + string column = impact[&quot;Column&quot;]?.ToString(); + string newValue = impact[&quot;NewValue&quot;]?.ToString(); + + switch (opType) + { + case &quot;update&quot;: + ApplyUpdate(conn, table, primaryKey, column, newValue); + break; + case &quot;insert&quot;: + ApplyInsert(conn, table, impact); + break; + case &quot;delete&quot;: + ApplyDelete(conn, table, primaryKey); + break; + } + } + + transaction.Commit(); + } + catch (Exception ex) + { + transaction.Rollback(); + Console.WriteLine($&quot;Error applying changes: {ex.Message}&quot;); + } + } + } + } + + private void ApplyUpdate(OracleConnection conn, string table, JObject primaryKey, string column, string newValue) + { + var pkConditions = BuildPrimaryKeyCondition(primaryKey); + var query = $&quot;UPDATE {table} SET {column} = :newValue WHERE {pkConditions}&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + cmd.Parameters.Add(new OracleParameter(&quot;newValue&quot;, newValue)); + cmd.ExecuteNonQuery(); + } + } + + private void ApplyInsert(OracleConnection conn, string table, JToken impact) + { + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + var newValues = (JObject)impact[&quot;NewValues&quot;]; + var columns = new List&lt;string&gt;(); + var values = new List&lt;string&gt;(); + + foreach (var property in primaryKey.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + foreach (var property in newValues.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + var query = $&quot;INSERT INTO {table} ({string.Join(&quot;, &quot;, columns)}) VALUES ({string.Join(&quot;, &quot;, values)})&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + foreach (var property in primaryKey.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + foreach (var property in newValues.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + cmd.ExecuteNonQuery(); + } + } + + private void ApplyDelete(OracleConnection conn, string table, JObject primaryKey) + { + var pkConditions = BuildPrimaryKeyCondition(primaryKey); + var query = $&quot;DELETE FROM {table} WHERE {pkConditions}&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + cmd.ExecuteNonQuery(); + } + } + + private string BuildPrimaryKeyCondition(JObject primaryKey) + { + var conditions = new List&lt;string&gt;(); + foreach (var prop in primaryKey.Properties()) + { + conditions.Add($&quot;{prop.Name} = :{prop.Name}&quot;); + } + return string.Join(&quot; AND &quot;, conditions); + } +} +</code></pre> +<ul> +<li><strong>ApplyChanges</strong>: Parses the JSON input and determines the operation type.</li> +<li><strong>ApplyUpdate</strong>: Executes an UPDATE SQL command using parameters to prevent SQL injection.</li> +<li><strong>ApplyInsert</strong>: Executes an INSERT SQL command, constructing columns and values from the JSON.</li> +<li><strong>ApplyDelete</strong>: Executes a DELETE SQL command based on the primary key. +BuildPrimaryKeyCondition: Constructs the WHERE clause for SQL commands.</li> +</ul> +<p>A side note, for the insert, you'll have the challenge if you are using auto-incremented IDs, this will mean you don't know the new IDs until you have inserted the data, so you should make sure to capture the new IDs and then create the audit log. This is left as a simple exercise to the reader in case it is necessary.</p> +<h2 id="csharp-to-revert-changes">CSharp to revert changes</h2> +<p>To revert changes (undo operations), we'll process the audit trail in reverse order. Here I give the processing of a list of operations as an example of unrolling. It is to note that the reverse delete does only one table, so if there was some connected information that was deleted via referential identity, it was the task of the audit table to keep that in the audit.</p> +<pre><code class="language-csharp">public class ChangeReverter +{ + public void RevertChanges(List&lt;string&gt; jsonOperations) + { + using (var conn = new OracleConnection(_connectionString)) + { + conn.Open(); + using (var transaction = conn.BeginTransaction()) + { + try + { + jsonOperations.Reverse(); // note: you could also have provided sorted by last time from the audit table instead of reversing them + + foreach (var operationJson in jsonOperations) + { + var operation = JObject.Parse(operationJson); + string opType = operation[&quot;Operation&quot;].ToString(); + var impactList = (JArray)operation[&quot;impact&quot;]; + + foreach (var impact in impactList) + { + string table = impact[&quot;Table&quot;].ToString(); + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + string column = impact[&quot;Column&quot;]?.ToString(); + string oldValue = impact[&quot;OldValue&quot;]?.ToString(); + + switch (opType) + { + case &quot;update&quot;: + RevertUpdate(conn, table, primaryKey, column, oldValue); + break; + case &quot;insert&quot;: + ApplyDelete(conn, table, primaryKey); + break; + case &quot;delete&quot;: + RevertDelete(conn, table, impact); + break; + } + } + } + + transaction.Commit(); + } + catch (Exception ex) + { + transaction.Rollback(); + Console.WriteLine($&quot;Error reverting changes: {ex.Message}&quot;); + } + } + } + } + + private void RevertUpdate(OracleConnection conn, string table, JObject primaryKey, string column, string oldValue) + { + var pkConditions = BuildPrimaryKeyCondition(primaryKey); + var query = $&quot;UPDATE {table} SET {column} = :oldValue WHERE {pkConditions}&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + cmd.Parameters.Add(new OracleParameter(&quot;oldValue&quot;, oldValue)); + cmd.ExecuteNonQuery(); + } + } + + private void RevertDelete(OracleConnection conn, string table, JToken impact) + { + var primaryKey = (JObject)impact[&quot;PrimaryKey&quot;]; + var oldValues = (JObject)impact[&quot;OldValues&quot;]; + var columns = new List&lt;string&gt;(); + var values = new List&lt;string&gt;(); + + foreach (var property in primaryKey.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + foreach (var property in oldValues.Properties()) + { + columns.Add(property.Name); + values.Add($&quot;:{property.Name}&quot;); + } + + var query = $&quot;INSERT INTO {table} ({string.Join(&quot;, &quot;, columns)}) VALUES ({string.Join(&quot;, &quot;, values)})&quot;; + + using (var cmd = new OracleCommand(query, conn)) + { + foreach (var property in primaryKey.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + foreach (var property in oldValues.Properties()) + { + cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString())); + } + + cmd.ExecuteNonQuery(); + } + } + + // Reuse BuildPrimaryKeyCondition and ApplyDelete methods from ChangeApplier +} +</code></pre> +<ul> +<li><strong>RevertChanges</strong>: Processes the list of JSON operations in reverse order to undo changes.</li> +<li><strong>RevertUpdate</strong>: Sets the column back to its old value.</li> +<li><strong>RevertDelete</strong>: Re-inserts a deleted row using the old values stored in the audit trail.</li> +<li><strong>ApplyDelete</strong>: Deletes a row, used here to undo an insert operation.</li> +</ul> +<h2 id="json-schema">JSON schema</h2> +<p>The reason that I prefer to use the Json directly in the C# code is that actually making up the C# classes for this schema is actually more work that processing the json directly in the code.</p> +<pre><code class="language-json">{ + &quot;$schema&quot;: &quot;http://json-schema.org/draft-07/schema#&quot;, + &quot;title&quot;: &quot;ImpactJsonRoot&quot;, + &quot;type&quot;: &quot;object&quot;, + &quot;properties&quot;: { + &quot;Operation&quot;: { + &quot;type&quot;: &quot;string&quot;, + &quot;enum&quot;: [&quot;update&quot;, &quot;insert&quot;, &quot;delete&quot;], + &quot;description&quot;: &quot;Type of operation&quot; + }, + &quot;Impact&quot;: { + &quot;type&quot;: &quot;array&quot;, + &quot;items&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;properties&quot;: { + &quot;Table&quot;: { + &quot;type&quot;: &quot;string&quot;, + &quot;description&quot;: &quot;Name of the table affected&quot; + }, + &quot;PrimaryKey&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;description&quot;: &quot;Primary key fields and their values&quot;, + &quot;additionalProperties&quot;: { + &quot;type&quot;: [&quot;number&quot;, &quot;null&quot;] + } + }, + &quot;Column&quot;: { + &quot;type&quot;: &quot;string&quot;, + &quot;description&quot;: &quot;Column affected (for updates)&quot; + }, + &quot;OldValue&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;], + &quot;description&quot;: &quot;Previous value (for updates and deletes)&quot; + }, + &quot;NewValue&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;], + &quot;description&quot;: &quot;New value (for updates and inserts)&quot; + }, + &quot;OldValues&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;description&quot;: &quot;All old values (for deletes)&quot;, + &quot;additionalProperties&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;] + } + }, + &quot;NewValues&quot;: { + &quot;type&quot;: &quot;object&quot;, + &quot;description&quot;: &quot;All new values (for inserts)&quot;, + &quot;additionalProperties&quot;: { + &quot;type&quot;: [&quot;string&quot;, &quot;number&quot;, &quot;boolean&quot;, &quot;null&quot;] + } + } + }, + &quot;required&quot;: [&quot;Table&quot;, &quot;PrimaryKey&quot;] + } + } + }, + &quot;required&quot;: [&quot;Operation&quot;, &quot;Impact&quot;] +} +</code></pre> +<p>and here are examples of operations:</p> +<h3 id="update">update</h3> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;update&quot;, + &quot;Impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: { &quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 2 }, + &quot;Column&quot;: &quot;Name&quot;, + &quot;OldValue&quot;: &quot;Old Instrument Name&quot;, + &quot;NewValue&quot;: &quot;Updated Instrument Name&quot; + } + ] +} + +</code></pre> +<h3 id="insert">insert</h3> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;insert&quot;, + &quot;Impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: { &quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 10 }, + &quot;NewValues&quot;: { + &quot;Name&quot;: &quot;New Instrument&quot;, + &quot;Type&quot;: &quot;Flexible Asset&quot;, + &quot;LastUpdated&quot;: &quot;2024-10-05T12:34:56Z&quot; + } + } + ] +} +</code></pre> +<h3 id="delete">delete</h3> +<pre><code class="language-json">{ + &quot;Operation&quot;: &quot;delete&quot;, + &quot;Impact&quot;: [ + { + &quot;Table&quot;: &quot;Instruments&quot;, + &quot;PrimaryKey&quot;: { &quot;ProjectID&quot;: 4, &quot;InstrumentID&quot;: 5 }, + &quot;OldValues&quot;: { + &quot;Name&quot;: &quot;Obsolete Instrument&quot;, + &quot;Type&quot;: &quot;Flexible Asset&quot;, + &quot;LastUpdated&quot;: &quot;2024-10-01T09:15:00Z&quot; + } + } + ] +} +</code></pre> +<p>Note: OpenAI's <code>o1-preview</code> was used to assist in the creation of the post.</p> + + VSCode Snippets http://ewinnington.github.io/posts/snippets-in-vscode @@ -1398,109 +1855,6 @@ for(int i = 0; i &lt; nItems; i++) c0.SetCoefficient(Items[i], weights[i]); SolveAndPrint(milp_solver, nItems, weights); </code></pre> <p>There are many ways of solving the knapsack problem, using LP and MILP solvers as seen here or using Dynamic Programming. The Google OR-Tools have a specific solver for multi-dimensional knapsack problems, including one which uses Dynamic Programming.</p> - - - - Hosting your C# Jupyter notebook online by adding one file to your repo - http://ewinnington.github.io/posts/my-binder-jupyter-csharp - <p><a href="https://mybinder.org/">MyBinder.org</a> in collaboration with <a href="https://github.com/dotnet/try">Dotnet try</a> allows you to host your .net notebooks online.</p> - http://ewinnington.github.io/posts/my-binder-jupyter-csharp - Thu, 14 Nov 2019 23:20:00 GMT - <p><a href="https://mybinder.org/">MyBinder.org</a> in collaboration with <a href="https://github.com/dotnet/try">Dotnet try</a> allows you to host your .net notebooks online.</p> -<p><a href="https://mybinder.org/v2/gh/ewinnington/noteb/master?filepath=SqliteInteraction.ipynb">SQLite example workbook: </a> -<a href="https://mybinder.org/v2/gh/ewinnington/noteb/master?filepath=SqliteInteraction.ipynb"><img src="https://mybinder.org/badge_logo.svg" class="img-fluid" alt="Binder" /></a></p> -<p>To light up this for your own hosted repositories, you will need a public github repo. Inside the repository, you will need to create a <a href="https://www.docker.com/">Docker</a> file that gives the setup required for MyBinder to setup the environment of the workbook.</p> -<p>The <a href="https://github.com/dotnet/try/blob/master/CreateBinder.md">dotnet/try</a> has the set of instrunctions.</p> -<p>For my repository, I used the following <a href="https://github.com/ewinnington/noteb/blob/master/Dockerfile">Dockerfile</a></p> -<p>A list of my changes to the standard one proposed by dotnet/try:</p> -<ul> -<li>I used a fixed docker image <code>jupyter/scipy-notebook:45f07a14b422</code></li> -<li>Since I have all my notebooks in the root of my repository I did <code>COPY . ${HOME}/Notebooks/</code></li> -<li>Since I am always importing the Nuget files at the top of my workbook, I did not need to have the docker deamon add a nuget config. So I commented out the COPY command <code># COPY ./NuGet.config ${HOME}/nuget.config</code></li> -<li>I commented out the custom <code>--add-source &quot;https://dotnet.myget.org/F/dotnet-try/api/v3/index.json&quot;</code> from the installation of the dotnet try tool, since I had issue with the nuget feed with the pre-release version. Installing with <code>RUN dotnet tool install -g dotnet-try</code> will get you the latest released version.</li> -</ul> -<pre><code class="language-Skip">FROM jupyter/scipy-notebook:45f07a14b422 - -# Install .NET CLI dependencies - -ARG NB_USER=jovyan -ARG NB_UID=1000 -ENV USER ${NB_USER} -ENV NB_UID ${NB_UID} -ENV HOME /home/${NB_USER} - -WORKDIR ${HOME} - -USER root -RUN apt-get update -RUN apt-get install -y curl - -# Install .NET CLI dependencies -RUN apt-get install -y --no-install-recommends \ - libc6 \ - libgcc1 \ - libgssapi-krb5-2 \ - libicu60 \ - libssl1.1 \ - libstdc++6 \ - zlib1g - -RUN rm -rf /var/lib/apt/lists/* - -# Install .NET Core SDK -ENV DOTNET_SDK_VERSION 3.0.100 - -RUN curl -SL --output dotnet.tar.gz https://dotnetcli.blob.core.windows.net/dotnet/Sdk/$DOTNET_SDK_VERSION/dotnet-sdk-$DOTNET_SDK_VERSION-linux-x64.tar.gz \ - &amp;&amp; dotnet_sha512='766da31f9a0bcfbf0f12c91ea68354eb509ac2111879d55b656f19299c6ea1c005d31460dac7c2a4ef82b3edfea30232c82ba301fb52c0ff268d3e3a1b73d8f7' \ - &amp;&amp; echo &quot;$dotnet_sha512 dotnet.tar.gz&quot; | sha512sum -c - \ - &amp;&amp; mkdir -p /usr/share/dotnet \ - &amp;&amp; tar -zxf dotnet.tar.gz -C /usr/share/dotnet \ - &amp;&amp; rm dotnet.tar.gz \ - &amp;&amp; ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet - -# Enable detection of running in a container -ENV DOTNET_RUNNING_IN_CONTAINER=true \ - # Enable correct mode for dotnet watch (only mode supported in a container) - DOTNET_USE_POLLING_FILE_WATCHER=true \ - # Skip extraction of XML docs - generally not useful within an image/container - helps performance - NUGET_XMLDOC_MODE=skip \ - # Opt out of telemetry until after we install jupyter when building the image, this prevents caching of machine id - DOTNET_TRY_CLI_TELEMETRY_OPTOUT=true - -# Trigger first run experience by running arbitrary cmd -RUN dotnet help - -# Copy notebooks - -COPY . ${HOME}/Notebooks/ - -# Copy package sources - -# COPY ./NuGet.config ${HOME}/nuget.config - -RUN chown -R ${NB_UID} ${HOME} -USER ${USER} - -# Install Microsoft.DotNet.Interactive -RUN dotnet tool install -g dotnet-try -#--add-source &quot;https://dotnet.myget.org/F/dotnet-try/api/v3/index.json&quot; - -ENV PATH=&quot;${PATH}:${HOME}/.dotnet/tools&quot; -RUN echo &quot;$PATH&quot; - -# Install kernel specs -RUN dotnet try jupyter install - -# Enable telemetry once we install jupyter for the image -ENV DOTNET_TRY_CLI_TELEMETRY_OPTOUT=false - -# Set root to Notebooks -WORKDIR ${HOME}/Notebooks/ -</code></pre> -<p>Once the Dockerfile is in the repository. Head over to <a href="https://mybinder.org/">MyBinder.org</a> and enter the link to your repository. Optionally, you can set an initial ipynb file to start when the link is clicked.</p> -<p><img src="/posts/images/my-binder/Binder-1.png" class="img-fluid" alt="MyBinder" /></p> -<p>When you click &quot;launch&quot;, MyBinder will download your repository and start the docker build, very soon you will be able to access your binders online. Fully shareable and totally awesome!</p> -<p><img src="/posts/images/my-binder/Binder-2.png" class="img-fluid" width="60%" alt="SQLite Running" /></p> diff --git a/index.html b/index.html index 51bebee..539ebf3 100644 --- a/index.html +++ b/index.html @@ -94,6 +94,462 @@

A collection of thoughts, code and snippets.

+
+ +

Using an audit trail table on Oracle

+
+

Posted on Saturday, 5 October 2024

+

Implementing Auditable Updates in a Relational Database

+

In modern applications, maintaining an audit trail of changes to data is crucial for compliance, debugging, and data integrity. This blog post explores a straightforward approach to implementing auditable updates in a relational database system, specifically focusing on a project management scenario with hierarchical data.

+

Problem Description

+

We have a relational database containing Projects, each of which includes Instruments, Markets, and Valuations. These entities form a tree structure, adhering to the third normal form (3NF). Previously, any update to a project involved downloading the entire project tree, making changes, and uploading a new project under a new ID to ensure complete auditability.

+

This approach is inefficient for small updates and doesn't allow for granular tracking of changes. The goal is to enable small, precise updates to projects while maintaining a comprehensive audit trail of all changes.

+

Solution Overview

+

We introduce an audit table that records every change made to the database. The audit table will store serialized JSON representations of operations like update, insert, and delete. We'll also provide C# code to apply and revert these changes, effectively creating an undo stack.

+

Let's use the following DB Schema for illustration:

+

TableVide

+
    +
  • Primary Keys: Each table has a primary key (e.g., ProjectID, InstrumentID).
  • +
  • Foreign Keys: Child tables reference their parent via foreign keys (e.g., instruments.ProjectID references projects.ProjectID).
  • +
  • Audit Table: The change_audit table records changes with fields like ChangeAuditID, TimeApplied, and ImpactJson.
  • +
+ +

Implementing Change Auditing

+

The change_audit table is designed to store all changes in a JSON format for flexibility and ease of storage.

+
CREATE TABLE change_audit (
+  ChangeAuditID   NUMBER PRIMARY KEY,
+  TimeApplied     TIMESTAMP,
+  UserID          VARCHAR2(100),
+  ImpactJson      CLOB
+);
+
+

JSON Structure for Changes

+

Each change is recorded as a JSON object:

+
{
+  "Operation": "update",
+  "impact": [
+    {
+      "Table": "Instruments",
+      "PrimaryKey": {"ProjectID": 4, "InstrumentID": 2},
+      "Column": "Name",
+      "OldValue": "Old Instrument Name",
+      "NewValue": "Updated Instrument Name"
+    }
+  ]
+}
+
+

CSharp to apply changes given an operation

+

To apply changes recorded in the JSON, we'll use C# code that parses the JSON and executes the corresponding SQL commands.

+

I assume you have the _connectionString available somewhere as a constant in the code.

+
using Oracle.ManagedDataAccess.Client;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+
+public class ChangeApplier
+{
+
+    public void ApplyChanges(string jsonInput)
+    {
+        // Parse the JSON input
+        var operation = JObject.Parse(jsonInput);
+        string opType = operation["Operation"].ToString();
+        var impactList = (JArray)operation["impact"];
+
+        using (var conn = new OracleConnection(_connectionString))
+        {
+            conn.Open();
+            using (var transaction = conn.BeginTransaction())
+            {
+                try
+                {
+                    foreach (var impact in impactList)
+                    {
+                        string table = impact["Table"].ToString();
+                        var primaryKey = (JObject)impact["PrimaryKey"];
+                        string column = impact["Column"]?.ToString();
+                        string newValue = impact["NewValue"]?.ToString();
+
+                        switch (opType)
+                        {
+                            case "update":
+                                ApplyUpdate(conn, table, primaryKey, column, newValue);
+                                break;
+                            case "insert":
+                                ApplyInsert(conn, table, impact);
+                                break;
+                            case "delete":
+                                ApplyDelete(conn, table, primaryKey);
+                                break;
+                        }
+                    }
+
+                    transaction.Commit();
+                }
+                catch (Exception ex)
+                {
+                    transaction.Rollback();
+                    Console.WriteLine($"Error applying changes: {ex.Message}");
+                }
+            }
+        }
+    }
+
+    private void ApplyUpdate(OracleConnection conn, string table, JObject primaryKey, string column, string newValue)
+    {
+        var pkConditions = BuildPrimaryKeyCondition(primaryKey);
+        var query = $"UPDATE {table} SET {column} = :newValue WHERE {pkConditions}";
+
+        using (var cmd = new OracleCommand(query, conn))
+        {
+            cmd.Parameters.Add(new OracleParameter("newValue", newValue));
+            cmd.ExecuteNonQuery();
+        }
+    }
+
+    private void ApplyInsert(OracleConnection conn, string table, JToken impact)
+    {
+        var primaryKey = (JObject)impact["PrimaryKey"];
+        var newValues = (JObject)impact["NewValues"];
+        var columns = new List<string>();
+        var values = new List<string>();
+
+        foreach (var property in primaryKey.Properties())
+        {
+            columns.Add(property.Name);
+            values.Add($":{property.Name}");
+        }
+
+        foreach (var property in newValues.Properties())
+        {
+            columns.Add(property.Name);
+            values.Add($":{property.Name}");
+        }
+
+        var query = $"INSERT INTO {table} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
+
+        using (var cmd = new OracleCommand(query, conn))
+        {
+            foreach (var property in primaryKey.Properties())
+            {
+                cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString()));
+            }
+
+            foreach (var property in newValues.Properties())
+            {
+                cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString()));
+            }
+
+            cmd.ExecuteNonQuery();
+        }
+    }
+
+    private void ApplyDelete(OracleConnection conn, string table, JObject primaryKey)
+    {
+        var pkConditions = BuildPrimaryKeyCondition(primaryKey);
+        var query = $"DELETE FROM {table} WHERE {pkConditions}";
+
+        using (var cmd = new OracleCommand(query, conn))
+        {
+            cmd.ExecuteNonQuery();
+        }
+    }
+
+    private string BuildPrimaryKeyCondition(JObject primaryKey)
+    {
+        var conditions = new List<string>();
+        foreach (var prop in primaryKey.Properties())
+        {
+            conditions.Add($"{prop.Name} = :{prop.Name}");
+        }
+        return string.Join(" AND ", conditions);
+    }
+}
+
+
    +
  • ApplyChanges: Parses the JSON input and determines the operation type.
  • +
  • ApplyUpdate: Executes an UPDATE SQL command using parameters to prevent SQL injection.
  • +
  • ApplyInsert: Executes an INSERT SQL command, constructing columns and values from the JSON.
  • +
  • ApplyDelete: Executes a DELETE SQL command based on the primary key. +BuildPrimaryKeyCondition: Constructs the WHERE clause for SQL commands.
  • +
+

A side note, for the insert, you'll have the challenge if you are using auto-incremented IDs, this will mean you don't know the new IDs until you have inserted the data, so you should make sure to capture the new IDs and then create the audit log. This is left as a simple exercise to the reader in case it is necessary.

+

CSharp to revert changes

+

To revert changes (undo operations), we'll process the audit trail in reverse order. Here I give the processing of a list of operations as an example of unrolling. It is to note that the reverse delete does only one table, so if there was some connected information that was deleted via referential identity, it was the task of the audit table to keep that in the audit.

+
public class ChangeReverter
+{
+    public void RevertChanges(List<string> jsonOperations)
+    {
+        using (var conn = new OracleConnection(_connectionString))
+        {
+            conn.Open();
+            using (var transaction = conn.BeginTransaction())
+            {
+                try
+                {
+                    jsonOperations.Reverse(); // note: you could also have provided sorted by last time from the audit table instead of reversing them
+
+                    foreach (var operationJson in jsonOperations)
+                    {
+                        var operation = JObject.Parse(operationJson);
+                        string opType = operation["Operation"].ToString();
+                        var impactList = (JArray)operation["impact"];
+
+                        foreach (var impact in impactList)
+                        {
+                            string table = impact["Table"].ToString();
+                            var primaryKey = (JObject)impact["PrimaryKey"];
+                            string column = impact["Column"]?.ToString();
+                            string oldValue = impact["OldValue"]?.ToString();
+
+                            switch (opType)
+                            {
+                                case "update":
+                                    RevertUpdate(conn, table, primaryKey, column, oldValue);
+                                    break;
+                                case "insert":
+                                    ApplyDelete(conn, table, primaryKey);
+                                    break;
+                                case "delete":
+                                    RevertDelete(conn, table, impact);
+                                    break;
+                            }
+                        }
+                    }
+
+                    transaction.Commit();
+                }
+                catch (Exception ex)
+                {
+                    transaction.Rollback();
+                    Console.WriteLine($"Error reverting changes: {ex.Message}");
+                }
+            }
+        }
+    }
+
+    private void RevertUpdate(OracleConnection conn, string table, JObject primaryKey, string column, string oldValue)
+    {
+        var pkConditions = BuildPrimaryKeyCondition(primaryKey);
+        var query = $"UPDATE {table} SET {column} = :oldValue WHERE {pkConditions}";
+
+        using (var cmd = new OracleCommand(query, conn))
+        {
+            cmd.Parameters.Add(new OracleParameter("oldValue", oldValue));
+            cmd.ExecuteNonQuery();
+        }
+    }
+
+    private void RevertDelete(OracleConnection conn, string table, JToken impact)
+    {
+        var primaryKey = (JObject)impact["PrimaryKey"];
+        var oldValues = (JObject)impact["OldValues"];
+        var columns = new List<string>();
+        var values = new List<string>();
+
+        foreach (var property in primaryKey.Properties())
+        {
+            columns.Add(property.Name);
+            values.Add($":{property.Name}");
+        }
+
+        foreach (var property in oldValues.Properties())
+        {
+            columns.Add(property.Name);
+            values.Add($":{property.Name}");
+        }
+
+        var query = $"INSERT INTO {table} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
+
+        using (var cmd = new OracleCommand(query, conn))
+        {
+            foreach (var property in primaryKey.Properties())
+            {
+                cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString()));
+            }
+
+            foreach (var property in oldValues.Properties())
+            {
+                cmd.Parameters.Add(new OracleParameter(property.Name, property.Value.ToString()));
+            }
+
+            cmd.ExecuteNonQuery();
+        }
+    }
+
+    // Reuse BuildPrimaryKeyCondition and ApplyDelete methods from ChangeApplier
+}
+
+
    +
  • RevertChanges: Processes the list of JSON operations in reverse order to undo changes.
  • +
  • RevertUpdate: Sets the column back to its old value.
  • +
  • RevertDelete: Re-inserts a deleted row using the old values stored in the audit trail.
  • +
  • ApplyDelete: Deletes a row, used here to undo an insert operation.
  • +
+

JSON schema

+

The reason that I prefer to use the Json directly in the C# code is that actually making up the C# classes for this schema is actually more work that processing the json directly in the code.

+
{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ImpactJsonRoot",
+  "type": "object",
+  "properties": {
+    "Operation": {
+      "type": "string",
+      "enum": ["update", "insert", "delete"],
+      "description": "Type of operation"
+    },
+    "Impact": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "Table": {
+            "type": "string",
+            "description": "Name of the table affected"
+          },
+          "PrimaryKey": {
+            "type": "object",
+            "description": "Primary key fields and their values",
+            "additionalProperties": {
+              "type": ["number", "null"]
+            }
+          },
+          "Column": {
+            "type": "string",
+            "description": "Column affected (for updates)"
+          },
+          "OldValue": {
+            "type": ["string", "number", "boolean", "null"],
+            "description": "Previous value (for updates and deletes)"
+          },
+          "NewValue": {
+            "type": ["string", "number", "boolean", "null"],
+            "description": "New value (for updates and inserts)"
+          },
+          "OldValues": {
+            "type": "object",
+            "description": "All old values (for deletes)",
+            "additionalProperties": {
+              "type": ["string", "number", "boolean", "null"]
+            }
+          },
+          "NewValues": {
+            "type": "object",
+            "description": "All new values (for inserts)",
+            "additionalProperties": {
+              "type": ["string", "number", "boolean", "null"]
+            }
+          }
+        },
+        "required": ["Table", "PrimaryKey"]
+      }
+    }
+  },
+  "required": ["Operation", "Impact"]
+}
+
+

and here are examples of operations:

+

update

+
{
+  "Operation": "update",
+  "Impact": [
+    {
+      "Table": "Instruments",
+      "PrimaryKey": { "ProjectID": 4, "InstrumentID": 2 },
+      "Column": "Name",
+      "OldValue": "Old Instrument Name",
+      "NewValue": "Updated Instrument Name"
+    }
+  ]
+}
+
+
+

insert

+
{
+  "Operation": "insert",
+  "Impact": [
+    {
+      "Table": "Instruments",
+      "PrimaryKey": { "ProjectID": 4, "InstrumentID": 10 },
+      "NewValues": {
+        "Name": "New Instrument",
+        "Type": "Flexible Asset",
+        "LastUpdated": "2024-10-05T12:34:56Z"
+      }
+    }
+  ]
+}
+
+

delete

+
{
+  "Operation": "delete",
+  "Impact": [
+    {
+      "Table": "Instruments",
+      "PrimaryKey": { "ProjectID": 4, "InstrumentID": 5 },
+      "OldValues": {
+        "Name": "Obsolete Instrument",
+        "Type": "Flexible Asset",
+        "LastUpdated": "2024-10-01T09:15:00Z"
+      }
+    }
+  ]
+}
+
+

Note: OpenAI's o1-preview was used to assist in the creation of the post.

+
+

VSCode Snippets

@@ -250,94 +706,6 @@

JavaScript


-
- -

DevContainers - The future of developer environments

-
-

Posted on Monday, 24 July 2023

-

History

-

It's been years now that we've had Infrastructure as Code (IaC), Containers and Desired state Configuration (DsC) tools to do our deployments. But these have been mostly focused on the deployment side of things, with fewer tools on the developer side. On the dev machine, installing and maintaining the development tools and package dependencies has been in flux, both in windows where finally tools like Ninite, Chocolatey and Winget allow management of dev tools, and on the linux side, which was always quite well served with apt - but has also gained Snap, Flatpack and other package management tools. The thing is, sometimes you need more that one version of a particular tool, Python3.10 and Python3.11, Java9 and Java17, Dotnet 4.8 and Dotnet 6, to work on the various projects you have during the day. Sometimes, they work side by side very well and sometimes they don't. And when they don't, it can be a long process to figure out why and also very difficult to get help without resorting to having a clean image refresh and starting again to install your dependencies.

-

Since the end of the 2010s and the early 2020s, with the rise of web hosted IDEs, there has been a need to define ways to have a base image that contained the environment and tools needed to work. I remember running some in the mid 2010s - Nitrous.IO (2013-16) - that allowed you to use a base container and configure it to do remote development.

-

DevContainers

-

With the arrival of Docker on every desktop, Github's Cloudspaces and Visual Studio Code, there's been a new interest in this type of desired state environments with developer tooling. Microsoft published the DevContainer specification in early 2022 to formalize the language.

-

So how does it help us? Well, with a DevContainer, we can setup a new development environment on Premise (in VSCode), on the cloud VM (Azure+VM) or on a Codespace environment with a single file that ensures that we always have the tools we want and need installed. Starting to work is as easy as openining the connection and cloning the repo we need if the .devcontainer file is located inside.

-

DevContainer example

-

You can find below my personal DevContainer, it is setup with Git, Node, AzureCLI, Docker control of hose, Dotnet, Terraform, Java with Maven, Python3 and Postgresql. I also have the VSCode extensions directly configured so I can directly start using them when I connect. I also use the "postStartCommand": "nohup bash -c 'postgres &'" to run an instance of Postgresql directly inside the development container, so I can a directly have a DB to run requests against. And yes, this is a bit of a kitchen sink DevContainer, they can be smaller and more tailored to a project with only one or two of these features included, but here I use a generic one add added everything I use apart from the c++ and fortran compilers.

-
{
-    "name": "Erics-base-dev-container",
-    "image": "mcr.microsoft.com/devcontainers/base:debian",
- 
-    "features": {
-        "ghcr.io/devcontainers/features/git:1": {},
-        "ghcr.io/devcontainers/features/node:1": {},
-        "ghcr.io/devcontainers/features/azure-cli:1": {}, //azure-cli,
-        "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, //docker on host
-        "ghcr.io/devcontainers/features/dotnet:1": {}, //dotnet installed
-        "ghcr.io/devcontainers/features/terraform:1": {},
-        "ghcr.io/devcontainers/features/java:1": { "installMaven" : true },
-        "ghcr.io/devcontainers-contrib/features/postgres-asdf:1": {}
-    },
- 
-    // Configure tool-specific properties.
-    "customizations": {
-        // Configure properties specific to VS Code.
-        "vscode": {
-            "settings": {},
-            "extensions": [
-                "streetsidesoftware.code-spell-checker",
-                "ms-azuretools.vscode-docker",
-                "ms-dotnettools.csharp",
-                "HashiCorp.terraform",
-                "ms-azuretools.vscode-azureterraform",
-                "GitHub.copilot",
-                "GitHub.copilot-chat",
-                "vscjava.vscode-java-pack",
-                "ms-python.python"
-            ]
-        }
-    },
- 
-    // Use 'forwardPorts' to make a list of ports inside the container available locally.
-    // "forwardPorts": [3000],
- 
-    // Use 'portsAttributes' to set default properties for specific forwarded ports.
-    // More info: https://containers.dev/implementors/json_reference/#port-attributes
-    "portsAttributes": {
-        "3000": {
-            "label": "Hello Remote World",
-            "onAutoForward": "notify"
-        }
-    },
- 
-    // Use 'postCreateCommand' to run commands after the container is created.
-    "postCreateCommand": "",
- 
-    "postStartCommand": "nohup bash -c 'postgres &'"
- 
-    // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
-    // "remoteUser": "root"
-}
-
-

So how do you start with DevContainers?

-

There are 2 easy ways:

-
    -
  1. (remote) Github Codespaces -By going to my repo, you can click "Create Codespace on Master" and get a running VSCode in the cloud with all those tools setup instantly.
  2. -
-

(at first build, the image might take time)

-
    -
  1. (local) Docker + VS Code -Ensure you have the ms-vscode-remote.remote-containers extension installed in VS Code and Docker installed.
  2. -
-

Clone the repo https://github.com/ewinnington/DevContainerTemplate.git, then open it with VSCode. It should automatically detect the .devContainer and offer to rebuild the container image and open it up in the IDE for you.

-

Once that is done, you should have access to a complete environment at the state you specified.

-

What's the use for Developers at corporations where computers are locked down?

-

I think that providing developer windows machine with Git, Docker, WSL2 installed and using VS Code or another IDE that supports DevContainers is an excellent way forwards in providing a good fast and stable environment for developers to work faster and more efficiently. Using this configuration, any person showing up to a Hackathon would be able to start working in minutes after cloning a repository. It would really simplify daily operations, since every repo can provide the correct .DevContainer configuration, or teams can share a DevContainer basic configuration.

-

This all simplifies operations, makes developer experience more consistent and increases productivity since you can move faster from one development environment to another in minutes. OnPrem → Remote VM → Cloudspace and back in minutes, without any friction.

-

All in all, I'm convinced it is a tool that both IT support must understand and master how to best provide access to, and for developers to understand the devContainer to benefit from it.

-

Have you used DevContainers? What is your experience?

-
-
- -

Raspberry Pi update Arkos

+
+

Debugging tips

Posted on Thursday, 31 October 2013

- -

Debugging tips

+
+

Raspberry Pi update Arkos

Posted on Thursday, 31 October 2013

@@ -401,20 +407,20 @@

Making Fortran DLLs to interface with VBA in Excel

Posted on Thursday, 24 May 2012

- -

Styling a Checkbox as an Ellipse

+
+

Styling a ListView with a Horizontal ItemsPanel and a Header

Posted on Wednesday, 23 May 2012

- -

Styling a ListView with a Horizontal ItemsPanel and a Header

+
+

Styling a Checkbox as an Ellipse

Posted on Wednesday, 23 May 2012

- -

Javascript google maps basics

+
+

Python - CSV handling

Posted on Tuesday, 24 April 2012

@@ -425,14 +431,14 @@

Python - Neat little file http download routine

Posted on Tuesday, 24 April 2012

- -

Python - CSV handling

+
+

KML File structure

Posted on Tuesday, 24 April 2012

- -

KML File structure

+
+

Javascript google maps basics

Posted on Tuesday, 24 April 2012

@@ -455,8 +461,8 @@

Emotes on iPhones iOS5

Posted on Wednesday, 28 March 2012

- -

Getting my ip on my Raspberry pi for a script

+
+

Google doesn't need to worry

Posted on Saturday, 11 February 2012

@@ -473,8 +479,8 @@

WPF - DynamicDataDisplay - A Chart component that works

Posted on Saturday, 11 February 2012

- -

Google doesn't need to worry

+
+

Getting my ip on my Raspberry pi for a script

Posted on Saturday, 11 February 2012

diff --git a/sitemap.xml b/sitemap.xml index 360d510..c6426c5 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -http://ewinnington.github.io/abouthttp://ewinnington.github.io/videoshttp://ewinnington.github.io/posts/snippets-in-vscodehttp://ewinnington.github.io/posts/HowToUpdateWritebookhttp://ewinnington.github.io/posts/HttpClientCompressionhttp://ewinnington.github.io/posts/devcontainershttp://ewinnington.github.io/posts/Proxmoxhttp://ewinnington.github.io/posts/network-vpnhttp://ewinnington.github.io/posts/mini-pchttp://ewinnington.github.io/posts/learn-from-chatgpt-Crdt-OThttp://ewinnington.github.io/posts/7Reasons-no-cachehttp://ewinnington.github.io/posts/Software-Architecture-Illustrationhttp://ewinnington.github.io/posts/Data-Lineagehttp://ewinnington.github.io/posts/tesla-megapackhttp://ewinnington.github.io/posts/sqlite-microstoreshttp://ewinnington.github.io/posts/Starship-laser-ablationhttp://ewinnington.github.io/posts/Viruses-left-behindhttp://ewinnington.github.io/posts/db-healthhttp://ewinnington.github.io/posts/Point-of-exporing-spacehttp://ewinnington.github.io/posts/jupyter-tips-csharphttp://ewinnington.github.io/posts/jupyter-lp-20http://ewinnington.github.io/posts/my-binder-jupyter-csharphttp://ewinnington.github.io/posts/jupyter-lp-10http://ewinnington.github.io/posts/jupyter-docker-csharp-postgreshttp://ewinnington.github.io/posts/jupyter-sqlite-csharphttp://ewinnington.github.io/posts/jupyter-notebook-csharp-rhttp://ewinnington.github.io/posts/bulk-insertshttp://ewinnington.github.io/posts/webassembly-thoughtshttp://ewinnington.github.io/posts/R-read-binary-filehttp://ewinnington.github.io/posts/wyam-github-actionshttp://ewinnington.github.io/posts/R-Redis-Jsonhttp://ewinnington.github.io/posts/Switching-to-wyamhttp://ewinnington.github.io/posts/60-year-bethttp://ewinnington.github.io/posts/append-files-together-using-windows-command-linehttp://ewinnington.github.io/posts/tail-command-with-powershellhttp://ewinnington.github.io/posts/oracle-getting-a-list-of-all-columns-data-types-lengths-of-column-for-every-table-in-your-schemahttp://ewinnington.github.io/posts/getting-autoincremented-version-numbers-in-visual-studiohttp://ewinnington.github.io/posts/using-svg-images-on-azure-websiteshttp://ewinnington.github.io/posts/diff-compare-two-files-in-visual-studiohttp://ewinnington.github.io/posts/listing-the-channels-of-the-wifi-networks-around-you-in-windowshttp://ewinnington.github.io/posts/oracle-reclaiming-lob-space-after-deletion-clob-blobhttp://ewinnington.github.io/posts/oracle-sql-developer-set-nls-to-give-you-full-date-and-timehttp://ewinnington.github.io/posts/sqlfiddle-a-way-to-share-sql-snippets-so-that-they-can-be-tested-in-the-browserhttp://ewinnington.github.io/posts/raspberry-pi-update-arkoshttp://ewinnington.github.io/posts/debugging-tipshttp://ewinnington.github.io/posts/commenting-and-uncommenting-in-vihttp://ewinnington.github.io/posts/updating-my-raspberry-pis-ip-address-on-a-distant-server-via-sftphttp://ewinnington.github.io/posts/raspberry-pihttp://ewinnington.github.io/posts/making-fortran-dlls-to-interface-with-vba-in-excelhttp://ewinnington.github.io/posts/styling-a-checkbox-as-an-ellipsehttp://ewinnington.github.io/posts/styling-a-listview-with-a-horizontal-itemspanel-and-a-headerhttp://ewinnington.github.io/posts/javascript-google-maps-basicshttp://ewinnington.github.io/posts/python-neat-little-file-http-download-routinehttp://ewinnington.github.io/posts/python-csv-handlinghttp://ewinnington.github.io/posts/kml-file-structurehttp://ewinnington.github.io/posts/accessing-the-power-control-panel-in-windows-xphttp://ewinnington.github.io/posts/adding-windows-printers-with-a-vbscripthttp://ewinnington.github.io/posts/emotes-on-iphones-ios5http://ewinnington.github.io/posts/getting-my-ip-on-my-raspberrypi-for-a-scripthttp://ewinnington.github.io/posts/wpf-avalondock-a-docking-componenthttp://ewinnington.github.io/posts/wpf-dynamicdatadisplay-a-chart-component-that-workshttp://ewinnington.github.io/posts/google-doesnt-need-to-worryhttp://ewinnington.github.io/posts/Oracle10g-Pivoting-datahttp://ewinnington.github.io/posts/apple-apps-remotehttp://ewinnington.github.io/posts/migrating-my-mum-to-osx-adressbook-and-applescripthttp://ewinnington.github.io/posts/Awesomenote-and-Evernotehttp://ewinnington.github.io/posts/migrating-my-mum-to-os-x-experienceshttp://ewinnington.github.io/posts/one-app-that-changes-the-iphonehttp://ewinnington.github.io/posts/screen-and-paper-convergencehttp://ewinnington.github.io/posts/setting-up-wordpress-publishing-from-iphonehttp://ewinnington.github.io/posts/the-livescribe-echo-penhttp://ewinnington.github.io/posts/migrating-my-mum-os-xhttp://ewinnington.github.io/tags/VBAhttp://ewinnington.github.io/tags/MapsAPIhttp://ewinnington.github.io/tags/Rpihttp://ewinnington.github.io/tags/Websiteshttp://ewinnington.github.io/tags/VisualStudiohttp://ewinnington.github.io/tags/Migratedhttp://ewinnington.github.io/tags/Wyamhttp://ewinnington.github.io/tags/markdownhttp://ewinnington.github.io/tags/WinXPhttp://ewinnington.github.io/tags/Google-OR-Toolshttp://ewinnington.github.io/tags/MariaDbhttp://ewinnington.github.io/tags/Oraclehttp://ewinnington.github.io/tags/Databasehttp://ewinnington.github.io/tags/Javascripthttp://ewinnington.github.io/tags/Batteryhttp://ewinnington.github.io/tags/KMLhttp://ewinnington.github.io/tags/Debugginghttp://ewinnington.github.io/tags/Virushttp://ewinnington.github.io/tags/SQLhttp://ewinnington.github.io/tags/DataLineagehttp://ewinnington.github.io/tags/Googlehttp://ewinnington.github.io/tags/Kuberneteshttp://ewinnington.github.io/tags/Proxmoxhttp://ewinnington.github.io/tags/Githubhttp://ewinnington.github.io/tags/DevContainershttp://ewinnington.github.io/tags/CSharphttp://ewinnington.github.io/tags/Wordpresshttp://ewinnington.github.io/tags/OSXhttp://ewinnington.github.io/tags/iOShttp://ewinnington.github.io/tags/VBScripthttp://ewinnington.github.io/tags/Wifihttp://ewinnington.github.io/tags/SVGhttp://ewinnington.github.io/tags/FileProcessinghttp://ewinnington.github.io/tags/GithubActionshttp://ewinnington.github.io/tags/Webassemblyhttp://ewinnington.github.io/tags/Jupyter-notebookhttp://ewinnington.github.io/tags/Postgresqlhttp://ewinnington.github.io/tags/Spacehttp://ewinnington.github.io/tags/SQLitehttp://ewinnington.github.io/tags/Redishttp://ewinnington.github.io/tags/VPNhttp://ewinnington.github.io/tags/Devhttp://ewinnington.github.io/tags/Dockerhttp://ewinnington.github.io/tags/Pythonhttp://ewinnington.github.io/tags/Cachinghttp://ewinnington.github.io/tags/Evernotehttp://ewinnington.github.io/tags/Charthttp://ewinnington.github.io/tags/Win10http://ewinnington.github.io/tags/Diffhttp://ewinnington.github.io/tags/Powershellhttp://ewinnington.github.io/tags/JSONhttp://ewinnington.github.io/tags/Binaryhttp://ewinnington.github.io/tags/Mixed-Integer-linear-programminghttp://ewinnington.github.io/tags/Dotnet-tryhttp://ewinnington.github.io/tags/MsSqlhttp://ewinnington.github.io/tags/Metalhttp://ewinnington.github.io/tags/Tailscalehttp://ewinnington.github.io/tags/Codespaceshttp://ewinnington.github.io/tags/JavaScripthttp://ewinnington.github.io/tags/Writebookhttp://ewinnington.github.io/tags/Thoughtshttp://ewinnington.github.io/tags/Awesomenotehttp://ewinnington.github.io/tags/DynamicDataDisplayhttp://ewinnington.github.io/tags/WPFhttp://ewinnington.github.io/tags/Vihttp://ewinnington.github.io/tags/Windowshttp://ewinnington.github.io/tags/Azurehttp://ewinnington.github.io/tags/CommandLinehttp://ewinnington.github.io/tags/GithubPageshttp://ewinnington.github.io/tags/BulkInsertshttp://ewinnington.github.io/tags/LinearProgramminghttp://ewinnington.github.io/tags/MySqlhttp://ewinnington.github.io/tags/SpaceXhttp://ewinnington.github.io/tags/Energyhttp://ewinnington.github.io/tags/chatGPThttp://ewinnington.github.io/tags/Networkhttp://ewinnington.github.io/tags/Architecturehttp://ewinnington.github.io/tags/VSCodehttp://ewinnington.github.io/tags/Rhttp://ewinnington.github.io/tags/vscodehttp://ewinnington.github.io/tags/Livescribehttp://ewinnington.github.io/tags/iOS5http://ewinnington.github.io/tags/Fortranhttp://ewinnington.github.io/tagshttp://ewinnington.github.io/postshttp://ewinnington.github.io/http://ewinnington.github.io/feed.atomhttp://ewinnington.github.io/feed.rsshttp://ewinnington.github.io/posts/snippets-in-vscodehttp://ewinnington.github.io/posts/HowToUpdateWritebookhttp://ewinnington.github.io/posts/HttpClientCompressionhttp://ewinnington.github.io/posts/devcontainershttp://ewinnington.github.io/posts/Proxmoxhttp://ewinnington.github.io/posts/network-vpnhttp://ewinnington.github.io/posts/mini-pchttp://ewinnington.github.io/posts/learn-from-chatgpt-Crdt-OThttp://ewinnington.github.io/posts/7Reasons-no-cachehttp://ewinnington.github.io/posts/Software-Architecture-Illustrationhttp://ewinnington.github.io/posts/Data-Lineagehttp://ewinnington.github.io/posts/tesla-megapackhttp://ewinnington.github.io/posts/sqlite-microstoreshttp://ewinnington.github.io/posts/Starship-laser-ablationhttp://ewinnington.github.io/posts/Viruses-left-behindhttp://ewinnington.github.io/posts/db-healthhttp://ewinnington.github.io/posts/Point-of-exporing-spacehttp://ewinnington.github.io/posts/jupyter-tips-csharphttp://ewinnington.github.io/posts/jupyter-lp-20http://ewinnington.github.io/posts/my-binder-jupyter-csharphttp://ewinnington.github.io/posts/jupyter-lp-10http://ewinnington.github.io/posts/jupyter-docker-csharp-postgreshttp://ewinnington.github.io/posts/jupyter-sqlite-csharphttp://ewinnington.github.io/posts/jupyter-notebook-csharp-rhttp://ewinnington.github.io/posts/bulk-insertshttp://ewinnington.github.io/posts/webassembly-thoughtshttp://ewinnington.github.io/posts/wyam-github-actionshttp://ewinnington.github.io/posts/R-Redis-Jsonhttp://ewinnington.github.io/posts/R-read-binary-filehttp://ewinnington.github.io/posts/Switching-to-wyamhttp://ewinnington.github.io/posts/60-year-bethttp://ewinnington.github.io/posts/append-files-together-using-windows-command-linehttp://ewinnington.github.io/posts/tail-command-with-powershellhttp://ewinnington.github.io/posts/oracle-getting-a-list-of-all-columns-data-types-lengths-of-column-for-every-table-in-your-schemahttp://ewinnington.github.io/posts/getting-autoincremented-version-numbers-in-visual-studiohttp://ewinnington.github.io/posts/using-svg-images-on-azure-websiteshttp://ewinnington.github.io/posts/diff-compare-two-files-in-visual-studiohttp://ewinnington.github.io/posts/listing-the-channels-of-the-wifi-networks-around-you-in-windowshttp://ewinnington.github.io/posts/oracle-reclaiming-lob-space-after-deletion-clob-blobhttp://ewinnington.github.io/posts/oracle-sql-developer-set-nls-to-give-you-full-date-and-timehttp://ewinnington.github.io/posts/sqlfiddle-a-way-to-share-sql-snippets-so-that-they-can-be-tested-in-the-browserhttp://ewinnington.github.io/posts/debugging-tipshttp://ewinnington.github.io/posts/raspberry-pi-update-arkoshttp://ewinnington.github.io/posts/commenting-and-uncommenting-in-vihttp://ewinnington.github.io/posts/updating-my-raspberry-pis-ip-address-on-a-distant-server-via-sftphttp://ewinnington.github.io/posts/raspberry-pihttp://ewinnington.github.io/posts/making-fortran-dlls-to-interface-with-vba-in-excelhttp://ewinnington.github.io/posts/styling-a-listview-with-a-horizontal-itemspanel-and-a-headerhttp://ewinnington.github.io/posts/styling-a-checkbox-as-an-ellipsehttp://ewinnington.github.io/posts/python-csv-handlinghttp://ewinnington.github.io/posts/python-neat-little-file-http-download-routinehttp://ewinnington.github.io/posts/kml-file-structurehttp://ewinnington.github.io/posts/javascript-google-maps-basicshttp://ewinnington.github.io/posts/accessing-the-power-control-panel-in-windows-xphttp://ewinnington.github.io/posts/adding-windows-printers-with-a-vbscripthttp://ewinnington.github.io/posts/emotes-on-iphones-ios5http://ewinnington.github.io/posts/getting-my-ip-on-my-raspberrypi-for-a-scripthttp://ewinnington.github.io/posts/wpf-avalondock-a-docking-componenthttp://ewinnington.github.io/posts/google-doesnt-need-to-worryhttp://ewinnington.github.io/posts/wpf-dynamicdatadisplay-a-chart-component-that-workshttp://ewinnington.github.io/posts/Oracle10g-Pivoting-datahttp://ewinnington.github.io/posts/apple-apps-remotehttp://ewinnington.github.io/posts/migrating-my-mum-to-osx-adressbook-and-applescripthttp://ewinnington.github.io/posts/migrating-my-mum-to-os-x-experienceshttp://ewinnington.github.io/posts/Awesomenote-and-Evernotehttp://ewinnington.github.io/posts/one-app-that-changes-the-iphonehttp://ewinnington.github.io/posts/screen-and-paper-convergencehttp://ewinnington.github.io/posts/setting-up-wordpress-publishing-from-iphonehttp://ewinnington.github.io/posts/the-livescribe-echo-penhttp://ewinnington.github.io/posts/migrating-my-mum-os-xhttp://ewinnington.github.io/abouthttp://ewinnington.github.io/videos \ No newline at end of file +http://ewinnington.github.io/abouthttp://ewinnington.github.io/videoshttp://ewinnington.github.io/posts/Audit-Trail-Oraclehttp://ewinnington.github.io/posts/snippets-in-vscodehttp://ewinnington.github.io/posts/HowToUpdateWritebookhttp://ewinnington.github.io/posts/HttpClientCompressionhttp://ewinnington.github.io/posts/devcontainershttp://ewinnington.github.io/posts/Proxmoxhttp://ewinnington.github.io/posts/network-vpnhttp://ewinnington.github.io/posts/mini-pchttp://ewinnington.github.io/posts/learn-from-chatgpt-Crdt-OThttp://ewinnington.github.io/posts/7Reasons-no-cachehttp://ewinnington.github.io/posts/Software-Architecture-Illustrationhttp://ewinnington.github.io/posts/Data-Lineagehttp://ewinnington.github.io/posts/tesla-megapackhttp://ewinnington.github.io/posts/sqlite-microstoreshttp://ewinnington.github.io/posts/Starship-laser-ablationhttp://ewinnington.github.io/posts/Viruses-left-behindhttp://ewinnington.github.io/posts/db-healthhttp://ewinnington.github.io/posts/Point-of-exporing-spacehttp://ewinnington.github.io/posts/jupyter-tips-csharphttp://ewinnington.github.io/posts/jupyter-lp-20http://ewinnington.github.io/posts/my-binder-jupyter-csharphttp://ewinnington.github.io/posts/jupyter-lp-10http://ewinnington.github.io/posts/jupyter-docker-csharp-postgreshttp://ewinnington.github.io/posts/jupyter-sqlite-csharphttp://ewinnington.github.io/posts/jupyter-notebook-csharp-rhttp://ewinnington.github.io/posts/bulk-insertshttp://ewinnington.github.io/posts/webassembly-thoughtshttp://ewinnington.github.io/posts/R-Redis-Jsonhttp://ewinnington.github.io/posts/R-read-binary-filehttp://ewinnington.github.io/posts/wyam-github-actionshttp://ewinnington.github.io/posts/Switching-to-wyamhttp://ewinnington.github.io/posts/60-year-bethttp://ewinnington.github.io/posts/append-files-together-using-windows-command-linehttp://ewinnington.github.io/posts/tail-command-with-powershellhttp://ewinnington.github.io/posts/oracle-getting-a-list-of-all-columns-data-types-lengths-of-column-for-every-table-in-your-schemahttp://ewinnington.github.io/posts/getting-autoincremented-version-numbers-in-visual-studiohttp://ewinnington.github.io/posts/using-svg-images-on-azure-websiteshttp://ewinnington.github.io/posts/diff-compare-two-files-in-visual-studiohttp://ewinnington.github.io/posts/listing-the-channels-of-the-wifi-networks-around-you-in-windowshttp://ewinnington.github.io/posts/oracle-reclaiming-lob-space-after-deletion-clob-blobhttp://ewinnington.github.io/posts/oracle-sql-developer-set-nls-to-give-you-full-date-and-timehttp://ewinnington.github.io/posts/sqlfiddle-a-way-to-share-sql-snippets-so-that-they-can-be-tested-in-the-browserhttp://ewinnington.github.io/posts/debugging-tipshttp://ewinnington.github.io/posts/raspberry-pi-update-arkoshttp://ewinnington.github.io/posts/commenting-and-uncommenting-in-vihttp://ewinnington.github.io/posts/updating-my-raspberry-pis-ip-address-on-a-distant-server-via-sftphttp://ewinnington.github.io/posts/raspberry-pihttp://ewinnington.github.io/posts/making-fortran-dlls-to-interface-with-vba-in-excelhttp://ewinnington.github.io/posts/styling-a-listview-with-a-horizontal-itemspanel-and-a-headerhttp://ewinnington.github.io/posts/styling-a-checkbox-as-an-ellipsehttp://ewinnington.github.io/posts/python-csv-handlinghttp://ewinnington.github.io/posts/python-neat-little-file-http-download-routinehttp://ewinnington.github.io/posts/kml-file-structurehttp://ewinnington.github.io/posts/javascript-google-maps-basicshttp://ewinnington.github.io/posts/accessing-the-power-control-panel-in-windows-xphttp://ewinnington.github.io/posts/adding-windows-printers-with-a-vbscripthttp://ewinnington.github.io/posts/emotes-on-iphones-ios5http://ewinnington.github.io/posts/google-doesnt-need-to-worryhttp://ewinnington.github.io/posts/wpf-avalondock-a-docking-componenthttp://ewinnington.github.io/posts/wpf-dynamicdatadisplay-a-chart-component-that-workshttp://ewinnington.github.io/posts/getting-my-ip-on-my-raspberrypi-for-a-scripthttp://ewinnington.github.io/posts/Oracle10g-Pivoting-datahttp://ewinnington.github.io/posts/apple-apps-remotehttp://ewinnington.github.io/posts/migrating-my-mum-to-osx-adressbook-and-applescripthttp://ewinnington.github.io/posts/Awesomenote-and-Evernotehttp://ewinnington.github.io/posts/migrating-my-mum-to-os-x-experienceshttp://ewinnington.github.io/posts/one-app-that-changes-the-iphonehttp://ewinnington.github.io/posts/screen-and-paper-convergencehttp://ewinnington.github.io/posts/setting-up-wordpress-publishing-from-iphonehttp://ewinnington.github.io/posts/the-livescribe-echo-penhttp://ewinnington.github.io/posts/migrating-my-mum-os-xhttp://ewinnington.github.io/tags/Githubhttp://ewinnington.github.io/tags/JavaScripthttp://ewinnington.github.io/tags/vscodehttp://ewinnington.github.io/tags/CSharphttp://ewinnington.github.io/tags/Wifihttp://ewinnington.github.io/tags/SVGhttp://ewinnington.github.io/tags/Awesomenotehttp://ewinnington.github.io/tags/Charthttp://ewinnington.github.io/tags/VBScripthttp://ewinnington.github.io/tags/VBAhttp://ewinnington.github.io/tags/Vihttp://ewinnington.github.io/tags/Proxmoxhttp://ewinnington.github.io/tags/Codespaceshttp://ewinnington.github.io/tags/DevContainershttp://ewinnington.github.io/tags/Writebookhttp://ewinnington.github.io/tags/Oraclehttp://ewinnington.github.io/tags/Windowshttp://ewinnington.github.io/tags/Azurehttp://ewinnington.github.io/tags/CommandLinehttp://ewinnington.github.io/tags/GithubPageshttp://ewinnington.github.io/tags/Webassemblyhttp://ewinnington.github.io/tags/Jupyter-notebookhttp://ewinnington.github.io/tags/Postgresqlhttp://ewinnington.github.io/tags/Spacehttp://ewinnington.github.io/tags/Batteryhttp://ewinnington.github.io/tags/Thoughtshttp://ewinnington.github.io/tags/Evernotehttp://ewinnington.github.io/tags/iOShttp://ewinnington.github.io/tags/iOS5http://ewinnington.github.io/tags/Win10http://ewinnington.github.io/tags/Googlehttp://ewinnington.github.io/tags/VPNhttp://ewinnington.github.io/tags/Devhttp://ewinnington.github.io/tags/VSCodehttp://ewinnington.github.io/tags/Pythonhttp://ewinnington.github.io/tags/markdownhttp://ewinnington.github.io/tags/Rpihttp://ewinnington.github.io/tags/Websiteshttp://ewinnington.github.io/tags/Powershellhttp://ewinnington.github.io/tags/GithubActionshttp://ewinnington.github.io/tags/Binaryhttp://ewinnington.github.io/tags/Google-OR-Toolshttp://ewinnington.github.io/tags/Dotnet-tryhttp://ewinnington.github.io/tags/MsSqlhttp://ewinnington.github.io/tags/SQLhttp://ewinnington.github.io/tags/DataLineagehttp://ewinnington.github.io/tags/chatGPThttp://ewinnington.github.io/tags/Livescribehttp://ewinnington.github.io/tags/OSXhttp://ewinnington.github.io/tags/DynamicDataDisplayhttp://ewinnington.github.io/tags/WinXPhttp://ewinnington.github.io/tags/KMLhttp://ewinnington.github.io/tags/Networkhttp://ewinnington.github.io/tags/Architecturehttp://ewinnington.github.io/tags/Dockerhttp://ewinnington.github.io/tags/Rhttp://ewinnington.github.io/tags/Databasehttp://ewinnington.github.io/tags/Debugginghttp://ewinnington.github.io/tags/Diffhttp://ewinnington.github.io/tags/VisualStudiohttp://ewinnington.github.io/tags/Migratedhttp://ewinnington.github.io/tags/JSONhttp://ewinnington.github.io/tags/Mixed-Integer-linear-programminghttp://ewinnington.github.io/tags/MariaDbhttp://ewinnington.github.io/tags/Virushttp://ewinnington.github.io/tags/SQLitehttp://ewinnington.github.io/tags/Redishttp://ewinnington.github.io/tags/Metalhttp://ewinnington.github.io/tags/MapsAPIhttp://ewinnington.github.io/tags/Fortranhttp://ewinnington.github.io/tags/Kuberneteshttp://ewinnington.github.io/tags/Tailscalehttp://ewinnington.github.io/tags/FileProcessinghttp://ewinnington.github.io/tags/Wyamhttp://ewinnington.github.io/tags/BulkInsertshttp://ewinnington.github.io/tags/LinearProgramminghttp://ewinnington.github.io/tags/MySqlhttp://ewinnington.github.io/tags/SpaceXhttp://ewinnington.github.io/tags/Energyhttp://ewinnington.github.io/tags/Cachinghttp://ewinnington.github.io/tags/Wordpresshttp://ewinnington.github.io/tags/Javascripthttp://ewinnington.github.io/tags/WPFhttp://ewinnington.github.io/tagshttp://ewinnington.github.io/postshttp://ewinnington.github.io/http://ewinnington.github.io/feed.atomhttp://ewinnington.github.io/feed.rsshttp://ewinnington.github.io/posts/Audit-Trail-Oraclehttp://ewinnington.github.io/posts/snippets-in-vscodehttp://ewinnington.github.io/posts/HowToUpdateWritebookhttp://ewinnington.github.io/posts/HttpClientCompressionhttp://ewinnington.github.io/posts/devcontainershttp://ewinnington.github.io/posts/Proxmoxhttp://ewinnington.github.io/posts/network-vpnhttp://ewinnington.github.io/posts/mini-pchttp://ewinnington.github.io/posts/learn-from-chatgpt-Crdt-OThttp://ewinnington.github.io/posts/7Reasons-no-cachehttp://ewinnington.github.io/posts/Software-Architecture-Illustrationhttp://ewinnington.github.io/posts/Data-Lineagehttp://ewinnington.github.io/posts/tesla-megapackhttp://ewinnington.github.io/posts/sqlite-microstoreshttp://ewinnington.github.io/posts/Starship-laser-ablationhttp://ewinnington.github.io/posts/Viruses-left-behindhttp://ewinnington.github.io/posts/db-healthhttp://ewinnington.github.io/posts/Point-of-exporing-spacehttp://ewinnington.github.io/posts/jupyter-tips-csharphttp://ewinnington.github.io/posts/jupyter-lp-20http://ewinnington.github.io/posts/my-binder-jupyter-csharphttp://ewinnington.github.io/posts/jupyter-lp-10http://ewinnington.github.io/posts/jupyter-docker-csharp-postgreshttp://ewinnington.github.io/posts/jupyter-sqlite-csharphttp://ewinnington.github.io/posts/jupyter-notebook-csharp-rhttp://ewinnington.github.io/posts/bulk-insertshttp://ewinnington.github.io/posts/webassembly-thoughtshttp://ewinnington.github.io/posts/wyam-github-actionshttp://ewinnington.github.io/posts/R-Redis-Jsonhttp://ewinnington.github.io/posts/R-read-binary-filehttp://ewinnington.github.io/posts/Switching-to-wyamhttp://ewinnington.github.io/posts/60-year-bethttp://ewinnington.github.io/posts/append-files-together-using-windows-command-linehttp://ewinnington.github.io/posts/tail-command-with-powershellhttp://ewinnington.github.io/posts/oracle-getting-a-list-of-all-columns-data-types-lengths-of-column-for-every-table-in-your-schemahttp://ewinnington.github.io/posts/getting-autoincremented-version-numbers-in-visual-studiohttp://ewinnington.github.io/posts/using-svg-images-on-azure-websiteshttp://ewinnington.github.io/posts/diff-compare-two-files-in-visual-studiohttp://ewinnington.github.io/posts/listing-the-channels-of-the-wifi-networks-around-you-in-windowshttp://ewinnington.github.io/posts/oracle-reclaiming-lob-space-after-deletion-clob-blobhttp://ewinnington.github.io/posts/oracle-sql-developer-set-nls-to-give-you-full-date-and-timehttp://ewinnington.github.io/posts/sqlfiddle-a-way-to-share-sql-snippets-so-that-they-can-be-tested-in-the-browserhttp://ewinnington.github.io/posts/debugging-tipshttp://ewinnington.github.io/posts/raspberry-pi-update-arkoshttp://ewinnington.github.io/posts/commenting-and-uncommenting-in-vihttp://ewinnington.github.io/posts/updating-my-raspberry-pis-ip-address-on-a-distant-server-via-sftphttp://ewinnington.github.io/posts/raspberry-pihttp://ewinnington.github.io/posts/making-fortran-dlls-to-interface-with-vba-in-excelhttp://ewinnington.github.io/posts/styling-a-listview-with-a-horizontal-itemspanel-and-a-headerhttp://ewinnington.github.io/posts/styling-a-checkbox-as-an-ellipsehttp://ewinnington.github.io/posts/kml-file-structurehttp://ewinnington.github.io/posts/python-csv-handlinghttp://ewinnington.github.io/posts/javascript-google-maps-basicshttp://ewinnington.github.io/posts/python-neat-little-file-http-download-routinehttp://ewinnington.github.io/posts/accessing-the-power-control-panel-in-windows-xphttp://ewinnington.github.io/posts/adding-windows-printers-with-a-vbscripthttp://ewinnington.github.io/posts/emotes-on-iphones-ios5http://ewinnington.github.io/posts/wpf-dynamicdatadisplay-a-chart-component-that-workshttp://ewinnington.github.io/posts/google-doesnt-need-to-worryhttp://ewinnington.github.io/posts/getting-my-ip-on-my-raspberrypi-for-a-scripthttp://ewinnington.github.io/posts/wpf-avalondock-a-docking-componenthttp://ewinnington.github.io/posts/Oracle10g-Pivoting-datahttp://ewinnington.github.io/posts/apple-apps-remotehttp://ewinnington.github.io/posts/migrating-my-mum-to-osx-adressbook-and-applescripthttp://ewinnington.github.io/posts/Awesomenote-and-Evernotehttp://ewinnington.github.io/posts/migrating-my-mum-to-os-x-experienceshttp://ewinnington.github.io/posts/one-app-that-changes-the-iphonehttp://ewinnington.github.io/posts/screen-and-paper-convergencehttp://ewinnington.github.io/posts/the-livescribe-echo-penhttp://ewinnington.github.io/posts/setting-up-wordpress-publishing-from-iphonehttp://ewinnington.github.io/posts/migrating-my-mum-os-xhttp://ewinnington.github.io/abouthttp://ewinnington.github.io/videos \ No newline at end of file diff --git a/tags/Architecture.html b/tags/Architecture.html index 13f97f8..38b5a73 100644 --- a/tags/Architecture.html +++ b/tags/Architecture.html @@ -147,18 +147,18 @@

Data Lineage for dataflow and workflow processes

diff --git a/tags/Awesomenote.html b/tags/Awesomenote.html index 14ed548..1bb95ae 100644 --- a/tags/Awesomenote.html +++ b/tags/Awesomenote.html @@ -111,18 +111,18 @@

Awesomenote and Evernote

diff --git a/tags/Azure.html b/tags/Azure.html index a0704ac..9176825 100644 --- a/tags/Azure.html +++ b/tags/Azure.html @@ -111,18 +111,18 @@

Using SVG images on Azure Websites

diff --git a/tags/Battery.html b/tags/Battery.html index 868aa18..dd2b671 100644 --- a/tags/Battery.html +++ b/tags/Battery.html @@ -111,18 +111,18 @@

Tesla Megapacks put into context

diff --git a/tags/Binary.html b/tags/Binary.html index 42f58fb..a7e4f81 100644 --- a/tags/Binary.html +++ b/tags/Binary.html @@ -111,18 +111,18 @@

R - Reading binary files

diff --git a/tags/BulkInserts.html b/tags/BulkInserts.html index 2c8435a..1aca483 100644 --- a/tags/BulkInserts.html +++ b/tags/BulkInserts.html @@ -111,18 +111,18 @@

Using bulk inserts on databases

diff --git a/tags/CSharp.html b/tags/CSharp.html index f19b5ca..df0ef5f 100644 --- a/tags/CSharp.html +++ b/tags/CSharp.html @@ -90,6 +90,12 @@

CSharp

+
+ +

Using an audit trail table on Oracle

+
+

Posted on Saturday, 5 October 2024

+

Receiving compressed data from an http(s) endpoint

@@ -159,18 +165,18 @@

Using bulk inserts on databases

diff --git a/tags/Caching.html b/tags/Caching.html index 330a8ca..8ea3833 100644 --- a/tags/Caching.html +++ b/tags/Caching.html @@ -111,18 +111,18 @@

7 reasons to not use caching

diff --git a/tags/Chart.html b/tags/Chart.html index 3ba5831..8fa42e3 100644 --- a/tags/Chart.html +++ b/tags/Chart.html @@ -111,18 +111,18 @@

WPF - DynamicDataDisplay - A Chart component that works

diff --git a/tags/Codespaces.html b/tags/Codespaces.html index 1f11803..f49cb70 100644 --- a/tags/Codespaces.html +++ b/tags/Codespaces.html @@ -111,18 +111,18 @@

DevContainers - The future of developer environments

diff --git a/tags/CommandLine.html b/tags/CommandLine.html index 20388cf..9bb4156 100644 --- a/tags/CommandLine.html +++ b/tags/CommandLine.html @@ -129,18 +129,18 @@

Adding windows printers with a VBScript

diff --git a/tags/DataLineage.html b/tags/DataLineage.html index 6e835ca..ef4b0be 100644 --- a/tags/DataLineage.html +++ b/tags/DataLineage.html @@ -111,18 +111,18 @@

Data Lineage for dataflow and workflow processes

diff --git a/tags/Database.html b/tags/Database.html index a9ded31..9490484 100644 --- a/tags/Database.html +++ b/tags/Database.html @@ -90,6 +90,12 @@

Database

+
+ +

Using an audit trail table on Oracle

+
+

Posted on Saturday, 5 October 2024

+

Embracing SQLite and living with micro-services

@@ -165,18 +171,18 @@

Oracle 10g - Pivoting data

diff --git a/tags/Debugging.html b/tags/Debugging.html index 1b7f696..57cbbac 100644 --- a/tags/Debugging.html +++ b/tags/Debugging.html @@ -111,18 +111,18 @@

Debugging tips

diff --git a/tags/Dev.html b/tags/Dev.html index cd6bfed..1ccbf6d 100644 --- a/tags/Dev.html +++ b/tags/Dev.html @@ -111,18 +111,18 @@

DevContainers - The future of developer environments

diff --git a/tags/DevContainers.html b/tags/DevContainers.html index 67d6b83..47a816b 100644 --- a/tags/DevContainers.html +++ b/tags/DevContainers.html @@ -111,18 +111,18 @@

DevContainers - The future of developer environments

diff --git a/tags/Diff.html b/tags/Diff.html index d7ca23c..fcedd82 100644 --- a/tags/Diff.html +++ b/tags/Diff.html @@ -111,18 +111,18 @@

Diff / Compare two files in Visual Studio

diff --git a/tags/Docker.html b/tags/Docker.html index c195aa7..2a9f55d 100644 --- a/tags/Docker.html +++ b/tags/Docker.html @@ -123,18 +123,18 @@

Docker controlled from Jupyter Notebook C# with PostgresDB

diff --git a/tags/Dotnet-try.html b/tags/Dotnet-try.html index 64c0838..92ecc14 100644 --- a/tags/Dotnet-try.html +++ b/tags/Dotnet-try.html @@ -147,18 +147,18 @@

Jupyter notebook with C# and R

diff --git a/tags/DynamicDataDisplay.html b/tags/DynamicDataDisplay.html index 6f03752..99e71e0 100644 --- a/tags/DynamicDataDisplay.html +++ b/tags/DynamicDataDisplay.html @@ -111,18 +111,18 @@

WPF - DynamicDataDisplay - A Chart component that works

diff --git a/tags/Energy.html b/tags/Energy.html index 8304d6f..f066773 100644 --- a/tags/Energy.html +++ b/tags/Energy.html @@ -111,18 +111,18 @@

Tesla Megapacks put into context

diff --git a/tags/Evernote.html b/tags/Evernote.html index 9a797a2..64f2ef5 100644 --- a/tags/Evernote.html +++ b/tags/Evernote.html @@ -111,18 +111,18 @@

Awesomenote and Evernote

diff --git a/tags/FileProcessing.html b/tags/FileProcessing.html index 9ab99dc..59ab244 100644 --- a/tags/FileProcessing.html +++ b/tags/FileProcessing.html @@ -111,18 +111,18 @@

Append files together using windows command line

diff --git a/tags/Fortran.html b/tags/Fortran.html index 090023c..449bfc4 100644 --- a/tags/Fortran.html +++ b/tags/Fortran.html @@ -111,18 +111,18 @@

Making Fortran DLLs to interface with VBA in Excel

diff --git a/tags/Github.html b/tags/Github.html index 2f9280f..1a97299 100644 --- a/tags/Github.html +++ b/tags/Github.html @@ -117,18 +117,18 @@

Using Github actions to build Wyam and publish to github pages

diff --git a/tags/GithubActions.html b/tags/GithubActions.html index 97fcdcb..19c36fa 100644 --- a/tags/GithubActions.html +++ b/tags/GithubActions.html @@ -111,18 +111,18 @@

Using Github actions to build Wyam and publish to github pages

diff --git a/tags/GithubPages.html b/tags/GithubPages.html index 7c39d87..d3045f4 100644 --- a/tags/GithubPages.html +++ b/tags/GithubPages.html @@ -111,18 +111,18 @@

Using Github actions to build Wyam and publish to github pages

diff --git a/tags/Google-OR-Tools.html b/tags/Google-OR-Tools.html index 5bd46d6..78c4931 100644 --- a/tags/Google-OR-Tools.html +++ b/tags/Google-OR-Tools.html @@ -117,18 +117,18 @@

Introduction to linear programming with C# and OR-Tools inside Jupyter

diff --git a/tags/Google.html b/tags/Google.html index 16d2aab..5356eb3 100644 --- a/tags/Google.html +++ b/tags/Google.html @@ -117,18 +117,18 @@

Google doesn't need to worry

diff --git a/tags/JSON.html b/tags/JSON.html index 4d79f37..ad9194a 100644 --- a/tags/JSON.html +++ b/tags/JSON.html @@ -111,18 +111,18 @@

R - Reading JSON from redis

diff --git a/tags/JavaScript.html b/tags/JavaScript.html index f3a960e..2f57084 100644 --- a/tags/JavaScript.html +++ b/tags/JavaScript.html @@ -111,18 +111,18 @@

Receiving compressed data from an http(s) endpoint

diff --git a/tags/Javascript.html b/tags/Javascript.html index c174a98..5dccc78 100644 --- a/tags/Javascript.html +++ b/tags/Javascript.html @@ -111,18 +111,18 @@

Javascript google maps basics

diff --git a/tags/Jupyter-notebook.html b/tags/Jupyter-notebook.html index cbcb520..c62c577 100644 --- a/tags/Jupyter-notebook.html +++ b/tags/Jupyter-notebook.html @@ -147,18 +147,18 @@

Jupyter notebook with C# and R

diff --git a/tags/KML.html b/tags/KML.html index 9ca9839..0461467 100644 --- a/tags/KML.html +++ b/tags/KML.html @@ -111,18 +111,18 @@

KML File structure

diff --git a/tags/Kubernetes.html b/tags/Kubernetes.html index 6e6d24a..6b40461 100644 --- a/tags/Kubernetes.html +++ b/tags/Kubernetes.html @@ -111,18 +111,18 @@

The era of the sub $200 PC

diff --git a/tags/LinearProgramming.html b/tags/LinearProgramming.html index a939ac2..9f53f57 100644 --- a/tags/LinearProgramming.html +++ b/tags/LinearProgramming.html @@ -117,18 +117,18 @@

Introduction to linear programming with C# and OR-Tools inside Jupyter

diff --git a/tags/Livescribe.html b/tags/Livescribe.html index 5784556..c1cb7fd 100644 --- a/tags/Livescribe.html +++ b/tags/Livescribe.html @@ -117,18 +117,18 @@

The Livescribe Echo pen

diff --git a/tags/MapsAPI.html b/tags/MapsAPI.html index eb77936..2ee23e1 100644 --- a/tags/MapsAPI.html +++ b/tags/MapsAPI.html @@ -111,18 +111,18 @@

Javascript google maps basics

diff --git a/tags/MariaDb.html b/tags/MariaDb.html index 1bca2ec..3af4c97 100644 --- a/tags/MariaDb.html +++ b/tags/MariaDb.html @@ -111,18 +111,18 @@

Checking for liveness on databases for health checks

diff --git a/tags/Metal.html b/tags/Metal.html index dddb3c2..cee79fd 100644 --- a/tags/Metal.html +++ b/tags/Metal.html @@ -111,18 +111,18 @@

The era of the sub $200 PC

diff --git a/tags/Migrated.html b/tags/Migrated.html index a6eaf46..a8bacba 100644 --- a/tags/Migrated.html +++ b/tags/Migrated.html @@ -151,14 +151,14 @@

SqlFiddle - A way to share SQL snippets so that they can be tested in the br

Posted on Tuesday, 5 November 2013

- -

Raspberry Pi update Arkos

+
+

Debugging tips

Posted on Thursday, 31 October 2013

- -

Debugging tips

+
+

Raspberry Pi update Arkos

Posted on Thursday, 31 October 2013

@@ -187,20 +187,20 @@

Making Fortran DLLs to interface with VBA in Excel

Posted on Thursday, 24 May 2012

- -

Styling a Checkbox as an Ellipse

+
+

Styling a ListView with a Horizontal ItemsPanel and a Header

Posted on Wednesday, 23 May 2012

- -

Styling a ListView with a Horizontal ItemsPanel and a Header

+
+

Styling a Checkbox as an Ellipse

Posted on Wednesday, 23 May 2012

- -

Javascript google maps basics

+
+

Python - CSV handling

Posted on Tuesday, 24 April 2012

@@ -211,14 +211,14 @@

Python - Neat little file http download routine

Posted on Tuesday, 24 April 2012

- -

Python - CSV handling

+
+

KML File structure

Posted on Tuesday, 24 April 2012

- -

KML File structure

+
+

Javascript google maps basics

Posted on Tuesday, 24 April 2012

@@ -241,8 +241,8 @@

Emotes on iPhones iOS5

Posted on Wednesday, 28 March 2012

- -

Getting my ip on my Raspberry pi for a script

+
+

Google doesn't need to worry

Posted on Saturday, 11 February 2012

@@ -259,8 +259,8 @@

WPF - DynamicDataDisplay - A Chart component that works

Posted on Saturday, 11 February 2012

- -

Google doesn't need to worry

+
+

Getting my ip on my Raspberry pi for a script

Posted on Saturday, 11 February 2012

@@ -333,18 +333,18 @@

Migrating my Mum (OS X)

diff --git a/tags/Mixed-Integer-linear-programming.html b/tags/Mixed-Integer-linear-programming.html index b046082..30bdd95 100644 --- a/tags/Mixed-Integer-linear-programming.html +++ b/tags/Mixed-Integer-linear-programming.html @@ -111,18 +111,18 @@

Applications in LP and MILP with C# and OR-Tools inside Jupyter

diff --git a/tags/MsSql.html b/tags/MsSql.html index a4ccc91..b1eabae 100644 --- a/tags/MsSql.html +++ b/tags/MsSql.html @@ -117,18 +117,18 @@

Using bulk inserts on databases

diff --git a/tags/MySql.html b/tags/MySql.html index 9b06f54..e14415f 100644 --- a/tags/MySql.html +++ b/tags/MySql.html @@ -111,18 +111,18 @@

Checking for liveness on databases for health checks

diff --git a/tags/Network.html b/tags/Network.html index c246623..c8e4bc1 100644 --- a/tags/Network.html +++ b/tags/Network.html @@ -111,18 +111,18 @@

Network planning and VPN

diff --git a/tags/OSX.html b/tags/OSX.html index 074fef9..26e054b 100644 --- a/tags/OSX.html +++ b/tags/OSX.html @@ -123,18 +123,18 @@

Migrating my Mum (OS X)

diff --git a/tags/Oracle.html b/tags/Oracle.html index a36a60b..06dfeef 100644 --- a/tags/Oracle.html +++ b/tags/Oracle.html @@ -90,6 +90,12 @@

Oracle

+
+ +

Using an audit trail table on Oracle

+
+

Posted on Saturday, 5 October 2024

+

Checking for liveness on databases for health checks

@@ -141,18 +147,18 @@

Oracle 10g - Pivoting data

diff --git a/tags/Postgresql.html b/tags/Postgresql.html index ae79c0f..a13443d 100644 --- a/tags/Postgresql.html +++ b/tags/Postgresql.html @@ -123,18 +123,18 @@

Using bulk inserts on databases

diff --git a/tags/Powershell.html b/tags/Powershell.html index d93c7c0..9475843 100644 --- a/tags/Powershell.html +++ b/tags/Powershell.html @@ -111,18 +111,18 @@

Tail command with powershell

diff --git a/tags/Proxmox.html b/tags/Proxmox.html index e066189..0d3cb25 100644 --- a/tags/Proxmox.html +++ b/tags/Proxmox.html @@ -111,18 +111,18 @@

Proxmox 8 on sub $200 mini PCs

diff --git a/tags/Python.html b/tags/Python.html index 992ca19..20872f5 100644 --- a/tags/Python.html +++ b/tags/Python.html @@ -97,14 +97,14 @@

Receiving compressed data from an http(s) endpoint

Posted on Wednesday, 20 March 2024

- -

Python - Neat little file http download routine

+
+

Python - CSV handling

Posted on Tuesday, 24 April 2012

- -

Python - CSV handling

+
+

Python - Neat little file http download routine

Posted on Tuesday, 24 April 2012

@@ -123,18 +123,18 @@

Python - CSV handling

diff --git a/tags/R.html b/tags/R.html index 4ac2187..ad57786 100644 --- a/tags/R.html +++ b/tags/R.html @@ -103,14 +103,14 @@

Jupyter notebook with C# and R

Posted on Tuesday, 12 November 2019

- -

R - Reading binary files

+
+

R - Reading JSON from redis

Posted on Sunday, 3 November 2019

- -

R - Reading JSON from redis

+
+

R - Reading binary files

Posted on Sunday, 3 November 2019

@@ -129,18 +129,18 @@

R - Reading JSON from redis

diff --git a/tags/Redis.html b/tags/Redis.html index db24d36..71496aa 100644 --- a/tags/Redis.html +++ b/tags/Redis.html @@ -117,18 +117,18 @@

R - Reading JSON from redis

diff --git a/tags/Rpi.html b/tags/Rpi.html index b113ac8..55deb7c 100644 --- a/tags/Rpi.html +++ b/tags/Rpi.html @@ -129,18 +129,18 @@

Getting my ip on my Raspberry pi for a script

diff --git a/tags/SQL.html b/tags/SQL.html index 7264666..d38b940 100644 --- a/tags/SQL.html +++ b/tags/SQL.html @@ -123,18 +123,18 @@

Oracle 10g - Pivoting data

diff --git a/tags/SQLite.html b/tags/SQLite.html index b393fad..dbdeef5 100644 --- a/tags/SQLite.html +++ b/tags/SQLite.html @@ -123,18 +123,18 @@

Testing SQLite in C# Jupyter notebook

diff --git a/tags/SVG.html b/tags/SVG.html index 92afbf3..ac160e6 100644 --- a/tags/SVG.html +++ b/tags/SVG.html @@ -111,18 +111,18 @@

Using SVG images on Azure Websites

diff --git a/tags/Space.html b/tags/Space.html index b608b69..0e594dd 100644 --- a/tags/Space.html +++ b/tags/Space.html @@ -129,18 +129,18 @@

60 year bet on the state of Humanity in Space

diff --git a/tags/SpaceX.html b/tags/SpaceX.html index 831ce7f..4517e95 100644 --- a/tags/SpaceX.html +++ b/tags/SpaceX.html @@ -111,18 +111,18 @@

The case for a SpaceX Starship laser ablation platform for orbital debris ma
diff --git a/tags/Tailscale.html b/tags/Tailscale.html index 42cfbfe..066f508 100644 --- a/tags/Tailscale.html +++ b/tags/Tailscale.html @@ -111,18 +111,18 @@

Proxmox 8 on sub $200 mini PCs

diff --git a/tags/Thoughts.html b/tags/Thoughts.html index 0a66985..af91f4c 100644 --- a/tags/Thoughts.html +++ b/tags/Thoughts.html @@ -165,18 +165,18 @@

60 year bet on the state of Humanity in Space

diff --git a/tags/VBA.html b/tags/VBA.html index c8fb42e..125309d 100644 --- a/tags/VBA.html +++ b/tags/VBA.html @@ -111,18 +111,18 @@

Making Fortran DLLs to interface with VBA in Excel

diff --git a/tags/VBScript.html b/tags/VBScript.html index 02e3b41..805ce23 100644 --- a/tags/VBScript.html +++ b/tags/VBScript.html @@ -111,18 +111,18 @@

Adding windows printers with a VBScript

diff --git a/tags/VPN.html b/tags/VPN.html index 73f32c1..3c792b0 100644 --- a/tags/VPN.html +++ b/tags/VPN.html @@ -111,18 +111,18 @@

Network planning and VPN

diff --git a/tags/VSCode.html b/tags/VSCode.html index 245b372..605febb 100644 --- a/tags/VSCode.html +++ b/tags/VSCode.html @@ -111,18 +111,18 @@

DevContainers - The future of developer environments

diff --git a/tags/Vi.html b/tags/Vi.html index f53ad1b..528e65a 100644 --- a/tags/Vi.html +++ b/tags/Vi.html @@ -111,18 +111,18 @@

Commenting and Uncommenting in vi

diff --git a/tags/Virus.html b/tags/Virus.html index 94cf373..557f492 100644 --- a/tags/Virus.html +++ b/tags/Virus.html @@ -111,18 +111,18 @@

We can leave viruses behind on Earth as we leave the gravity well

diff --git a/tags/VisualStudio.html b/tags/VisualStudio.html index 3263506..7383956 100644 --- a/tags/VisualStudio.html +++ b/tags/VisualStudio.html @@ -117,18 +117,18 @@

Diff / Compare two files in Visual Studio

diff --git a/tags/WPF.html b/tags/WPF.html index b5c3bb4..cc8fb65 100644 --- a/tags/WPF.html +++ b/tags/WPF.html @@ -91,14 +91,14 @@

WPF

- -

Styling a Checkbox as an Ellipse

+
+

Styling a ListView with a Horizontal ItemsPanel and a Header

Posted on Wednesday, 23 May 2012

- -

Styling a ListView with a Horizontal ItemsPanel and a Header

+
+

Styling a Checkbox as an Ellipse

Posted on Wednesday, 23 May 2012

@@ -129,18 +129,18 @@

WPF - DynamicDataDisplay - A Chart component that works

diff --git a/tags/Webassembly.html b/tags/Webassembly.html index dfad5a4..ca3d0bc 100644 --- a/tags/Webassembly.html +++ b/tags/Webassembly.html @@ -111,18 +111,18 @@

Reflections on Webassembly - May/Nov 19

diff --git a/tags/Websites.html b/tags/Websites.html index 2847a65..43ac14d 100644 --- a/tags/Websites.html +++ b/tags/Websites.html @@ -111,18 +111,18 @@

Using SVG images on Azure Websites

diff --git a/tags/Wifi.html b/tags/Wifi.html index 8a9b83b..96b32bc 100644 --- a/tags/Wifi.html +++ b/tags/Wifi.html @@ -111,18 +111,18 @@

Listing the channels of the WIFI networks around you in Windows

diff --git a/tags/Win10.html b/tags/Win10.html index 7578ab6..93e3f06 100644 --- a/tags/Win10.html +++ b/tags/Win10.html @@ -111,18 +111,18 @@

Accessing the power control panel in windows XP

diff --git a/tags/WinXP.html b/tags/WinXP.html index 1bfe4ee..dd11a9d 100644 --- a/tags/WinXP.html +++ b/tags/WinXP.html @@ -117,18 +117,18 @@

Adding windows printers with a VBScript

diff --git a/tags/Windows.html b/tags/Windows.html index bc6a5f1..8c0205d 100644 --- a/tags/Windows.html +++ b/tags/Windows.html @@ -111,18 +111,18 @@

Listing the channels of the WIFI networks around you in Windows

diff --git a/tags/Wordpress.html b/tags/Wordpress.html index 5f0fe9e..93f3bfb 100644 --- a/tags/Wordpress.html +++ b/tags/Wordpress.html @@ -111,18 +111,18 @@

Setting up Wordpress publishing from iPhone

diff --git a/tags/Writebook.html b/tags/Writebook.html index 52775c1..09b5e81 100644 --- a/tags/Writebook.html +++ b/tags/Writebook.html @@ -111,18 +111,18 @@

Adding LaTeX Maths to Writebook

diff --git a/tags/Wyam.html b/tags/Wyam.html index 84029a6..f284f11 100644 --- a/tags/Wyam.html +++ b/tags/Wyam.html @@ -117,18 +117,18 @@

Switching to Wyam

diff --git a/tags/chatGPT.html b/tags/chatGPT.html index d92a8dd..d7dc372 100644 --- a/tags/chatGPT.html +++ b/tags/chatGPT.html @@ -111,18 +111,18 @@

Learning concepts from chatGPT - Operational Transform and Conflict-free Rep
diff --git a/tags/iOS.html b/tags/iOS.html index 38a3533..d573e10 100644 --- a/tags/iOS.html +++ b/tags/iOS.html @@ -123,18 +123,18 @@

Setting up Wordpress publishing from iPhone

diff --git a/tags/iOS5.html b/tags/iOS5.html index b8b37d2..7c4dfe4 100644 --- a/tags/iOS5.html +++ b/tags/iOS5.html @@ -111,18 +111,18 @@

Emotes on iPhones iOS5

diff --git a/tags/index.html b/tags/index.html index ee2cf5b..97ce150 100644 --- a/tags/index.html +++ b/tags/index.html @@ -89,88 +89,88 @@

All Tags

diff --git a/tags/markdown.html b/tags/markdown.html index 236c4cb..9904e3f 100644 --- a/tags/markdown.html +++ b/tags/markdown.html @@ -117,18 +117,18 @@

Adding LaTeX Maths to Writebook

diff --git a/tags/vscode.html b/tags/vscode.html index 0a89f8f..72c0c2c 100644 --- a/tags/vscode.html +++ b/tags/vscode.html @@ -111,18 +111,18 @@

VSCode Snippets