Skip to content

Load a CSV file into a table ​

Task: load a product list into the database, all or nothing.

yaml
# Recipe: load the rows of a CSV file into a table, all or nothing.
paths:
  components: ./components
  migrations: ./migrations

datasources:
  db:
    driver: sqlite
    database: ./data/shop.db
sql
CREATE TABLE products (
    sku TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    price INTEGER NOT NULL CHECK (price > 0)
);
text
sku,name,price
MUG-1,Mug,30
TEE-1,T-shirt,80
STK-1,Sticker,5

The action reads the file with q:data, then inserts each row inside one q:transaction; the queries in it, in the loop too, use its datasource.

xml
<q:component name="Import">
  <q:action name="load" method="POST">
    <q:data name="rows" source="import/products.csv" type="csv">
      <q:column name="price" type="integer" />
    </q:data>

    <!-- One transaction for the whole file: if one row is refused (a sku
         already there, a price that is not positive), none stays. -->
    <q:transaction datasource="db">
      <q:loop items="{rows}" var="row">
        <q:query name="inserted">
          INSERT INTO products (sku, name, price) VALUES (:sku, :name, :price)
          <q:param name="sku" value="{row.sku}" type="string" />
          <q:param name="name" value="{row.name}" type="string" />
          <q:param name="price" value="{row.price}" type="integer" />
        </q:query>
      </q:loop>
    </q:transaction>

    <q:redirect url="/" flash="Loaded {len(rows)} products." />
  </q:action>

  <q:query name="products" datasource="db">SELECT sku, name, price FROM products ORDER BY sku</q:query>

  <ui:window title="Products">
    <q:if condition="flash">
      <ui:alert variant="success">{flash}</ui:alert>
    </q:if>
    <ui:text>{products_result.recordCount} products</ui:text>
    <ui:table source="{products}" />
    <ui:form on-submit="load" submit="Load import/products.csv" />
  </ui:window>
</q:component>

The second test loads the file when one of its products is already there: the second row fails, and the first, already inserted, is rolled back.

xml
<q:test name="the file's rows land in the table" page="/">
  <test:submit action="load" />
  <test:expect redirect="/" flash="Loaded 3 products." />
  <test:expect table="products" count="3" />
  <test:expect table="products" count="1" where="sku = 'TEE-1' AND price = 80" />
  <test:expect text="3 products" />
</q:test>

<q:test name="a file that fails halfway leaves nothing behind" page="/">
  <!-- TEE-1 is already there: the second row fails, so the first is undone too. -->
  <test:given table="products" sku="TEE-1" name="T-shirt" price="80" />
  <test:submit action="load" />
  <test:expect status="500" />
  <test:expect table="products" count="1" />
  <test:expect table="products" count="0" where="sku = 'MUG-1'" />
</q:test>
text
tests/load.test.q
  PASS  the file's rows land in the table
  PASS  a file that fails halfway leaves nothing behind
2 passed, 0 failed

See DATA-1 and DB-4.

MIT Licensed · Built with VitePress